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;
19use crate::execution::Artifact;
20use crate::execution::ArtifactGraph;
21use crate::execution::CapSubType;
22use crate::execution::MockConfig;
23use crate::execution::SKETCH_BLOCK_PARAM_ON;
24use crate::execution::cache::SketchModeState;
25use crate::execution::cache::clear_mem_cache;
26use crate::execution::cache::read_old_memory;
27use crate::execution::cache::write_old_memory;
28use crate::fmt::format_number_literal;
29use crate::front::Angle;
30use crate::front::ArcCtor;
31use crate::front::CircleCtor;
32use crate::front::Distance;
33use crate::front::EqualRadius;
34use crate::front::Error;
35use crate::front::ExecResult;
36use crate::front::FixedPoint;
37use crate::front::Freedom;
38use crate::front::LinesEqualLength;
39use crate::front::Midpoint;
40use crate::front::Object;
41use crate::front::Parallel;
42use crate::front::Perpendicular;
43use crate::front::PointCtor;
44use crate::front::Symmetric;
45use crate::front::Tangent;
46use crate::frontend::api::Expr;
47use crate::frontend::api::FileId;
48use crate::frontend::api::Number;
49use crate::frontend::api::ObjectId;
50use crate::frontend::api::ObjectKind;
51use crate::frontend::api::Plane;
52use crate::frontend::api::ProjectId;
53use crate::frontend::api::RestoreSketchCheckpointOutcome;
54use crate::frontend::api::SceneGraph;
55use crate::frontend::api::SceneGraphDelta;
56use crate::frontend::api::SketchCheckpointId;
57use crate::frontend::api::SourceDelta;
58use crate::frontend::api::SourceRef;
59use crate::frontend::api::Version;
60use crate::frontend::modify::find_defined_names;
61use crate::frontend::modify::next_free_name;
62use crate::frontend::modify::next_free_name_with_padding;
63use crate::frontend::sketch::Coincident;
64use crate::frontend::sketch::Constraint;
65use crate::frontend::sketch::ConstraintSegment;
66use crate::frontend::sketch::Diameter;
67use crate::frontend::sketch::ExistingSegmentCtor;
68use crate::frontend::sketch::Horizontal;
69use crate::frontend::sketch::LineCtor;
70use crate::frontend::sketch::Point2d;
71use crate::frontend::sketch::Radius;
72use crate::frontend::sketch::Segment;
73use crate::frontend::sketch::SegmentCtor;
74use crate::frontend::sketch::SketchApi;
75use crate::frontend::sketch::SketchCtor;
76use crate::frontend::sketch::Vertical;
77use crate::frontend::traverse::MutateBodyItem;
78use crate::frontend::traverse::TraversalReturn;
79use crate::frontend::traverse::Visitor;
80use crate::frontend::traverse::dfs_mut;
81use crate::id::IncIdGenerator;
82use crate::parsing::ast::types as ast;
83use crate::pretty::NumericSuffix;
84use crate::std::constraints::LinesAtAngleKind;
85use crate::walk::NodeMut;
86use crate::walk::Visitable;
87
88pub(crate) mod api;
89pub(crate) mod modify;
90pub(crate) mod sketch;
91
92pub const MAX_SKETCH_CHECKPOINTS: usize = 100;
93
94#[derive(Debug, Clone)]
95struct SketchCheckpoint {
96    id: SketchCheckpointId,
97    source: SourceDelta,
98    program: Program,
99    scene_graph: SceneGraph,
100    exec_outcome: ExecOutcome,
101    point_freedom_cache: HashMap<ObjectId, Freedom>,
102    mock_memory: Option<SketchModeState>,
103}
104mod traverse;
105pub(crate) mod trim;
106
107struct ArcSizeConstraintParams {
108    points: Vec<ObjectId>,
109    function_name: &'static str,
110    value: f64,
111    units: NumericSuffix,
112    label_position: Option<Point2d<Number>>,
113    constraint_type_name: &'static str,
114}
115
116const POINT_FN: &str = "point";
117const POINT_AT_PARAM: &str = "at";
118const LINE_FN: &str = "line";
119const LINE_VARIABLE: &str = "line";
120const LINE_START_PARAM: &str = "start";
121const LINE_END_PARAM: &str = "end";
122const ARC_FN: &str = "arc";
123const ARC_VARIABLE: &str = "arc";
124const ARC_START_PARAM: &str = "start";
125const ARC_END_PARAM: &str = "end";
126const ARC_CENTER_PARAM: &str = "center";
127const CIRCLE_FN: &str = "circle";
128const CIRCLE_VARIABLE: &str = "circle";
129const CIRCLE_START_PARAM: &str = "start";
130const CIRCLE_CENTER_PARAM: &str = "center";
131const LABEL_POSITION_PARAM: &str = "labelPosition";
132
133const COINCIDENT_FN: &str = "coincident";
134const DIAMETER_FN: &str = "diameter";
135const DISTANCE_FN: &str = "distance";
136const FIXED_FN: &str = "fixed";
137const ANGLE_FN: &str = "angle";
138const HORIZONTAL_DISTANCE_FN: &str = "horizontalDistance";
139const VERTICAL_DISTANCE_FN: &str = "verticalDistance";
140const EQUAL_LENGTH_FN: &str = "equalLength";
141const EQUAL_RADIUS_FN: &str = "equalRadius";
142const HORIZONTAL_FN: &str = "horizontal";
143const MIDPOINT_FN: &str = "midpoint";
144const MIDPOINT_POINT_PARAM: &str = "point";
145const RADIUS_FN: &str = "radius";
146const SYMMETRIC_FN: &str = "symmetric";
147const SYMMETRIC_AXIS_PARAM: &str = "axis";
148const TANGENT_FN: &str = "tangent";
149const VERTICAL_FN: &str = "vertical";
150
151const LINE_PROPERTY_START: &str = "start";
152const LINE_PROPERTY_END: &str = "end";
153
154const ARC_PROPERTY_START: &str = "start";
155const ARC_PROPERTY_END: &str = "end";
156const ARC_PROPERTY_CENTER: &str = "center";
157const CIRCLE_PROPERTY_START: &str = "start";
158const CIRCLE_PROPERTY_CENTER: &str = "center";
159
160const CONSTRUCTION_PARAM: &str = "construction";
161
162#[derive(Debug, Clone, Copy)]
163enum EditDeleteKind {
164    Edit,
165    DeleteNonSketch,
166}
167
168impl EditDeleteKind {
169    /// Returns true if this edit is any type of deletion.
170    fn is_delete(&self) -> bool {
171        match self {
172            EditDeleteKind::Edit => false,
173            EditDeleteKind::DeleteNonSketch => true,
174        }
175    }
176
177    fn to_change_kind(self) -> ChangeKind {
178        match self {
179            EditDeleteKind::Edit => ChangeKind::Edit,
180            EditDeleteKind::DeleteNonSketch => ChangeKind::Delete,
181        }
182    }
183}
184
185#[derive(Debug, Clone, Copy)]
186enum ChangeKind {
187    Add,
188    Edit,
189    Delete,
190    None,
191}
192
193#[derive(Debug, Clone, Serialize, ts_rs::TS)]
194#[ts(export, export_to = "FrontendApi.ts")]
195#[serde(tag = "type")]
196pub enum SetProgramOutcome {
197    #[serde(rename_all = "camelCase")]
198    Success {
199        scene_graph: Box<SceneGraph>,
200        exec_outcome: Box<ExecOutcome>,
201        checkpoint_id: Option<SketchCheckpointId>,
202    },
203    #[serde(rename_all = "camelCase")]
204    ExecFailure { error: Box<KclErrorWithOutputs> },
205}
206
207#[derive(Debug, Clone)]
208pub struct FrontendState {
209    program: Program,
210    scene_graph: SceneGraph,
211    /// Stores the last known freedom value for each point object.
212    /// This allows us to preserve freedom values when freedom analysis isn't run.
213    point_freedom_cache: HashMap<ObjectId, Freedom>,
214    sketch_checkpoints: VecDeque<SketchCheckpoint>,
215    sketch_checkpoint_id_gen: IncIdGenerator<u64>,
216}
217
218impl Default for FrontendState {
219    fn default() -> Self {
220        Self::new()
221    }
222}
223
224impl FrontendState {
225    pub fn new() -> Self {
226        Self {
227            program: Program::empty(),
228            scene_graph: SceneGraph {
229                project: ProjectId(0),
230                file: FileId(0),
231                version: Version(0),
232                objects: Default::default(),
233                settings: Default::default(),
234                sketch_mode: Default::default(),
235            },
236            point_freedom_cache: HashMap::new(),
237            sketch_checkpoints: VecDeque::new(),
238            sketch_checkpoint_id_gen: IncIdGenerator::new(1),
239        }
240    }
241
242    /// Get a reference to the scene graph
243    pub fn scene_graph(&self) -> &SceneGraph {
244        &self.scene_graph
245    }
246
247    pub fn default_length_unit(&self) -> UnitLength {
248        self.program
249            .meta_settings()
250            .ok()
251            .flatten()
252            .map(|settings| settings.default_length_units)
253            .unwrap_or(UnitLength::Millimeters)
254    }
255
256    pub async fn create_sketch_checkpoint(&mut self, exec_outcome: ExecOutcome) -> api::Result<SketchCheckpointId> {
257        let checkpoint_id = SketchCheckpointId::new(self.sketch_checkpoint_id_gen.next_id());
258
259        let checkpoint = SketchCheckpoint {
260            id: checkpoint_id,
261            source: SourceDelta {
262                text: source_from_ast(&self.program.ast),
263            },
264            program: self.program.clone(),
265            scene_graph: self.scene_graph.clone(),
266            exec_outcome,
267            point_freedom_cache: self.point_freedom_cache.clone(),
268            mock_memory: read_old_memory().await,
269        };
270
271        self.sketch_checkpoints.push_back(checkpoint);
272        while self.sketch_checkpoints.len() > MAX_SKETCH_CHECKPOINTS {
273            self.sketch_checkpoints.pop_front();
274        }
275
276        Ok(checkpoint_id)
277    }
278
279    pub async fn restore_sketch_checkpoint(
280        &mut self,
281        checkpoint_id: SketchCheckpointId,
282    ) -> api::Result<RestoreSketchCheckpointOutcome> {
283        let checkpoint = self
284            .sketch_checkpoints
285            .iter()
286            .find(|checkpoint| checkpoint.id == checkpoint_id)
287            .cloned()
288            .ok_or_else(|| Error {
289                msg: format!("Sketch checkpoint not found: {checkpoint_id:?}"),
290            })?;
291
292        self.program = checkpoint.program;
293        self.scene_graph = checkpoint.scene_graph.clone();
294        self.point_freedom_cache = checkpoint.point_freedom_cache;
295
296        if let Some(mock_memory) = checkpoint.mock_memory {
297            write_old_memory(mock_memory).await;
298        } else {
299            clear_mem_cache().await;
300        }
301
302        Ok(RestoreSketchCheckpointOutcome {
303            source_delta: checkpoint.source,
304            scene_graph_delta: SceneGraphDelta {
305                new_graph: checkpoint.scene_graph,
306                new_objects: Vec::new(),
307                invalidates_ids: true,
308                exec_outcome: checkpoint.exec_outcome,
309            },
310        })
311    }
312
313    pub fn clear_sketch_checkpoints(&mut self) {
314        self.sketch_checkpoints.clear();
315    }
316}
317
318impl SketchApi for FrontendState {
319    async fn execute_mock(
320        &mut self,
321        ctx: &ExecutorContext,
322        _version: Version,
323        sketch: ObjectId,
324    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
325        let sketch_block_ref =
326            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
327
328        let mut truncated_program = self.program.clone();
329        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::None)
330            .map_err(KclErrorWithOutputs::no_outputs)?;
331
332        // Execute.
333        let outcome = ctx
334            .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch))
335            .await?;
336        let new_source = source_from_ast(&self.program.ast);
337        let src_delta = SourceDelta { text: new_source };
338        // MockConfig::default() has freedom_analysis: true
339        let outcome = self.update_state_after_exec(outcome, true);
340        let scene_graph_delta = SceneGraphDelta {
341            new_graph: self.scene_graph.clone(),
342            new_objects: Default::default(),
343            invalidates_ids: false,
344            exec_outcome: outcome,
345        };
346        Ok((src_delta, scene_graph_delta))
347    }
348
349    async fn new_sketch(
350        &mut self,
351        ctx: &ExecutorContext,
352        _project: ProjectId,
353        _file: FileId,
354        _version: Version,
355        args: SketchCtor,
356    ) -> ExecResult<(SourceDelta, SceneGraphDelta, ObjectId)> {
357        // TODO: Check version.
358
359        let mut new_ast = self.program.ast.clone();
360        // Create updated KCL source from args.
361        let mut plane_ast =
362            sketch_on_ast_expr(&mut new_ast, &self.scene_graph, &args.on).map_err(KclErrorWithOutputs::no_outputs)?;
363        let mut defined_names = find_defined_names(&new_ast);
364        let is_face_of_expr = matches!(
365            &plane_ast,
366            ast::Expr::CallExpressionKw(call) if call.callee.name.name == "faceOf"
367        );
368        if is_face_of_expr {
369            let face_name = next_free_name_with_padding("face", &defined_names)
370                .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
371            let face_decl = ast::VariableDeclaration::new(
372                ast::VariableDeclarator::new(&face_name, plane_ast),
373                ast::ItemVisibility::Default,
374                ast::VariableKind::Const,
375            );
376            new_ast
377                .body
378                .push(ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(
379                    face_decl,
380                ))));
381            defined_names.insert(face_name.clone());
382            plane_ast = ast::Expr::Name(Box::new(ast::Name::new(&face_name)));
383        }
384        let sketch_ast = ast::SketchBlock {
385            arguments: vec![ast::LabeledArg {
386                label: Some(ast::Identifier::new(SKETCH_BLOCK_PARAM_ON)),
387                arg: plane_ast,
388            }],
389            body: Default::default(),
390            is_being_edited: false,
391            non_code_meta: Default::default(),
392            digest: None,
393        };
394        // Add a sketch block as a variable declaration directly, avoiding
395        // source-range mutation on a no-src node.
396        let sketch_name = next_free_name_with_padding("sketch", &defined_names)
397            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
398        let sketch_decl = ast::VariableDeclaration::new(
399            ast::VariableDeclarator::new(
400                &sketch_name,
401                ast::Expr::SketchBlock(Box::new(ast::Node::no_src(sketch_ast))),
402            ),
403            ast::ItemVisibility::Default,
404            ast::VariableKind::Const,
405        );
406        new_ast
407            .body
408            .push(ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(
409                sketch_decl,
410            ))));
411        // Convert to string source to create real source ranges.
412        let new_source = source_from_ast(&new_ast);
413        // Parse the new source.
414        let (new_program, errors) = Program::parse(&new_source)
415            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
416        if !errors.is_empty() {
417            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
418                "Error parsing KCL source after adding sketch: {errors:?}"
419            ))));
420        }
421        let Some(new_program) = new_program else {
422            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
423                "No AST produced after adding sketch".to_owned(),
424            )));
425        };
426
427        // Make sure to only set this if there are no errors.
428        self.program = new_program.clone();
429
430        // We need to do an engine execute so that the plane object gets created
431        // and is cached.
432        let outcome = ctx.run_with_caching(new_program.clone()).await?;
433        let freedom_analysis_ran = true;
434
435        let outcome = self.update_state_after_exec(outcome, freedom_analysis_ran);
436
437        let Some(sketch_id) = self
438            .scene_graph
439            .objects
440            .iter()
441            .filter_map(|object| match object.kind {
442                ObjectKind::Sketch(_) => Some(object.id),
443                _ => None,
444            })
445            .max_by_key(|id| id.0)
446        else {
447            return Err(KclErrorWithOutputs::from_error_outcome(
448                KclError::refactor("No objects in scene graph after adding sketch".to_owned()),
449                outcome,
450            ));
451        };
452        // Store the object in the scene.
453        self.scene_graph.sketch_mode = Some(sketch_id);
454
455        let src_delta = SourceDelta { text: new_source };
456        let scene_graph_delta = SceneGraphDelta {
457            new_graph: self.scene_graph.clone(),
458            invalidates_ids: false,
459            new_objects: vec![sketch_id],
460            exec_outcome: outcome,
461        };
462        Ok((src_delta, scene_graph_delta, sketch_id))
463    }
464
465    async fn edit_sketch(
466        &mut self,
467        ctx: &ExecutorContext,
468        _project: ProjectId,
469        _file: FileId,
470        _version: Version,
471        sketch: ObjectId,
472    ) -> ExecResult<SceneGraphDelta> {
473        // TODO: Check version.
474
475        // Look up existing sketch.
476        let sketch_object = self.scene_graph.objects.get(sketch.0).ok_or_else(|| {
477            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
478        })?;
479        let ObjectKind::Sketch(_) = &sketch_object.kind else {
480            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
481                "Object is not a sketch, it is {}",
482                sketch_object.kind.human_friendly_kind_with_article()
483            ))));
484        };
485        let sketch_block_ref = expect_single_node_ref(sketch_object).map_err(KclErrorWithOutputs::no_outputs)?;
486
487        // Enter sketch mode by setting the sketch_mode.
488        self.scene_graph.sketch_mode = Some(sketch);
489
490        // Truncate after the sketch block for mock execution.
491        let mut truncated_program = self.program.clone();
492        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::None)
493            .map_err(KclErrorWithOutputs::no_outputs)?;
494
495        // Execute in mock mode to ensure state is up to date. The caller will
496        // want freedom analysis to display segments correctly.
497        let outcome = ctx
498            .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch))
499            .await?;
500
501        // MockConfig::default() has freedom_analysis: true
502        let outcome = self.update_state_after_exec(outcome, true);
503        let scene_graph_delta = SceneGraphDelta {
504            new_graph: self.scene_graph.clone(),
505            invalidates_ids: false,
506            new_objects: Vec::new(),
507            exec_outcome: outcome,
508        };
509        Ok(scene_graph_delta)
510    }
511
512    async fn exit_sketch(
513        &mut self,
514        ctx: &ExecutorContext,
515        _version: Version,
516        sketch: ObjectId,
517    ) -> ExecResult<SceneGraph> {
518        // TODO: Check version.
519        #[cfg(not(target_arch = "wasm32"))]
520        let _ = sketch;
521        #[cfg(target_arch = "wasm32")]
522        if self.scene_graph.sketch_mode != Some(sketch) {
523            web_sys::console::warn_1(
524                &format!(
525                    "WARNING: exit_sketch: current state's sketch mode ID doesn't match the given sketch ID; state={:#?}, given={sketch:?}",
526                    &self.scene_graph.sketch_mode
527                )
528                .into(),
529            );
530        }
531        self.scene_graph.sketch_mode = None;
532
533        // Execute.
534        let outcome = ctx.run_with_caching(self.program.clone()).await?;
535
536        // exit_sketch doesn't run freedom analysis, just clears sketch_mode
537        self.update_state_after_exec(outcome, false);
538
539        Ok(self.scene_graph.clone())
540    }
541
542    async fn delete_sketch(
543        &mut self,
544        ctx: &ExecutorContext,
545        _version: Version,
546        sketch: ObjectId,
547    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
548        // TODO: Check version.
549
550        let mut new_ast = self.program.ast.clone();
551
552        // Look up existing sketch.
553        let sketch_id = sketch;
554        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
555            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
556        })?;
557        let ObjectKind::Sketch(_) = &sketch_object.kind else {
558            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
559                "Object is not a sketch, it is {}",
560                sketch_object.kind.human_friendly_kind_with_article(),
561            ))));
562        };
563
564        // Modify the AST to remove the sketch.
565        self.mutate_ast(&mut new_ast, sketch_id, AstMutateCommand::DeleteNode)
566            .map_err(KclErrorWithOutputs::no_outputs)?;
567
568        self.execute_after_delete_sketch(ctx, &mut new_ast).await
569    }
570
571    async fn add_segment(
572        &mut self,
573        ctx: &ExecutorContext,
574        _version: Version,
575        sketch: ObjectId,
576        segment: SegmentCtor,
577        _label: Option<String>,
578    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
579        // TODO: Check version.
580        match segment {
581            SegmentCtor::Point(ctor) => self.add_point(ctx, sketch, ctor).await,
582            SegmentCtor::Line(ctor) => self.add_line(ctx, sketch, ctor).await,
583            SegmentCtor::Arc(ctor) => self.add_arc(ctx, sketch, ctor).await,
584            SegmentCtor::Circle(ctor) => self.add_circle(ctx, sketch, ctor).await,
585        }
586    }
587
588    async fn edit_segments(
589        &mut self,
590        ctx: &ExecutorContext,
591        _version: Version,
592        sketch: ObjectId,
593        segments: Vec<ExistingSegmentCtor>,
594    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
595        // TODO: Check version.
596        let sketch_block_ref =
597            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
598
599        let mut new_ast = self.program.ast.clone();
600        let mut segment_ids_edited = AhashIndexSet::with_capacity_and_hasher(segments.len(), Default::default());
601
602        // segment_ids_edited still has to be the original segments (not final_edits), otherwise the owner segments
603        // are passed to `execute_after_edit` which changes the result of the solver, causing tests to fail.
604        for segment in &segments {
605            segment_ids_edited.insert(segment.id);
606        }
607
608        // Preprocess segments into a final_edits vector to handle if segments contains:
609        // - edit start point of line1 (as SegmentCtor::Point)
610        // - edit end point of line1 (as SegmentCtor::Point)
611        //
612        // This would result in only the end point to be updated because edit_point() clones line1's ctor from
613        // scene_graph, but this is still the old ctor because self.scene_graph is only updated after the loop finishes.
614        //
615        // To fix this, and other cases when the same point is edited from multiple elements in the segments Vec
616        // we apply all edits in order to final_edits in a way that owned point edits result in line edits,
617        // so the above example would result in a single line1 edit:
618        // - the first start point edit creates a new line edit entry in final_edits
619        // - the second end point edit finds this line edit and mutates the end position only.
620        //
621        // The result is that segments are flattened into a single IndexMap of edits by their owners, later edits overriding earlier ones.
622        let mut final_edits: IndexMap<ObjectId, SegmentCtor> = IndexMap::new();
623
624        for segment in segments {
625            let segment_id = segment.id;
626            match segment.ctor {
627                SegmentCtor::Point(ctor) => {
628                    // Find the owner, if any (point -> line / arc)
629                    if let Some(segment_object) = self.scene_graph.objects.get(segment_id.0)
630                        && let ObjectKind::Segment { segment } = &segment_object.kind
631                        && let Segment::Point(point) = segment
632                        && let Some(owner_id) = point.owner
633                        && let Some(owner_object) = self.scene_graph.objects.get(owner_id.0)
634                        && let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind
635                    {
636                        match owner_segment {
637                            Segment::Line(line) if line.start == segment_id || line.end == segment_id => {
638                                if let Some(existing) = final_edits.get_mut(&owner_id) {
639                                    let SegmentCtor::Line(line_ctor) = existing else {
640                                        return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
641                                            "Internal: Expected line ctor for owner, but found {}",
642                                            existing.human_friendly_kind_with_article()
643                                        ))));
644                                    };
645                                    // Line owner is already in final_edits -> apply this point edit
646                                    if line.start == segment_id {
647                                        line_ctor.start = ctor.position;
648                                    } else {
649                                        line_ctor.end = ctor.position;
650                                    }
651                                } else if let SegmentCtor::Line(line_ctor) = &line.ctor {
652                                    // Line owner is not in final_edits yet -> create it
653                                    let mut line_ctor = line_ctor.clone();
654                                    if line.start == segment_id {
655                                        line_ctor.start = ctor.position;
656                                    } else {
657                                        line_ctor.end = ctor.position;
658                                    }
659                                    final_edits.insert(owner_id, SegmentCtor::Line(line_ctor));
660                                } else {
661                                    // This should never run..
662                                    return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
663                                        "Internal: Line does not have line ctor, but found {}",
664                                        line.ctor.human_friendly_kind_with_article()
665                                    ))));
666                                }
667                                continue;
668                            }
669                            Segment::Arc(arc)
670                                if arc.start == segment_id || arc.end == segment_id || arc.center == segment_id =>
671                            {
672                                if let Some(existing) = final_edits.get_mut(&owner_id) {
673                                    let SegmentCtor::Arc(arc_ctor) = existing else {
674                                        return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
675                                            "Internal: Expected arc ctor for owner, but found {}",
676                                            existing.human_friendly_kind_with_article()
677                                        ))));
678                                    };
679                                    if arc.start == segment_id {
680                                        arc_ctor.start = ctor.position;
681                                    } else if arc.end == segment_id {
682                                        arc_ctor.end = ctor.position;
683                                    } else {
684                                        arc_ctor.center = ctor.position;
685                                    }
686                                } else if let SegmentCtor::Arc(arc_ctor) = &arc.ctor {
687                                    let mut arc_ctor = arc_ctor.clone();
688                                    if arc.start == segment_id {
689                                        arc_ctor.start = ctor.position;
690                                    } else if arc.end == segment_id {
691                                        arc_ctor.end = ctor.position;
692                                    } else {
693                                        arc_ctor.center = ctor.position;
694                                    }
695                                    final_edits.insert(owner_id, SegmentCtor::Arc(arc_ctor));
696                                } else {
697                                    return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
698                                        "Internal: Arc does not have arc ctor, but found {}",
699                                        arc.ctor.human_friendly_kind_with_article()
700                                    ))));
701                                }
702                                continue;
703                            }
704                            Segment::Circle(circle) if circle.start == segment_id || circle.center == segment_id => {
705                                if let Some(existing) = final_edits.get_mut(&owner_id) {
706                                    let SegmentCtor::Circle(circle_ctor) = existing else {
707                                        return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
708                                            "Internal: Expected circle ctor for owner, but found {}",
709                                            existing.human_friendly_kind_with_article()
710                                        ))));
711                                    };
712                                    if circle.start == segment_id {
713                                        circle_ctor.start = ctor.position;
714                                    } else {
715                                        circle_ctor.center = ctor.position;
716                                    }
717                                } else if let SegmentCtor::Circle(circle_ctor) = &circle.ctor {
718                                    let mut circle_ctor = circle_ctor.clone();
719                                    if circle.start == segment_id {
720                                        circle_ctor.start = ctor.position;
721                                    } else {
722                                        circle_ctor.center = ctor.position;
723                                    }
724                                    final_edits.insert(owner_id, SegmentCtor::Circle(circle_ctor));
725                                } else {
726                                    return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
727                                        "Internal: Circle does not have circle ctor, but found {}",
728                                        circle.ctor.human_friendly_kind_with_article()
729                                    ))));
730                                }
731                                continue;
732                            }
733                            _ => {}
734                        }
735                    }
736
737                    // No owner, it's an individual point
738                    final_edits.insert(segment_id, SegmentCtor::Point(ctor));
739                }
740                SegmentCtor::Line(ctor) => {
741                    final_edits.insert(segment_id, SegmentCtor::Line(ctor));
742                }
743                SegmentCtor::Arc(ctor) => {
744                    final_edits.insert(segment_id, SegmentCtor::Arc(ctor));
745                }
746                SegmentCtor::Circle(ctor) => {
747                    final_edits.insert(segment_id, SegmentCtor::Circle(ctor));
748                }
749            }
750        }
751
752        for (segment_id, ctor) in final_edits {
753            match ctor {
754                SegmentCtor::Point(ctor) => self
755                    .edit_point(&mut new_ast, sketch, segment_id, ctor)
756                    .map_err(KclErrorWithOutputs::no_outputs)?,
757                SegmentCtor::Line(ctor) => self
758                    .edit_line(&mut new_ast, sketch, segment_id, ctor)
759                    .map_err(KclErrorWithOutputs::no_outputs)?,
760                SegmentCtor::Arc(ctor) => self
761                    .edit_arc(&mut new_ast, sketch, segment_id, ctor)
762                    .map_err(KclErrorWithOutputs::no_outputs)?,
763                SegmentCtor::Circle(ctor) => self
764                    .edit_circle(&mut new_ast, sketch, segment_id, ctor)
765                    .map_err(KclErrorWithOutputs::no_outputs)?,
766            }
767        }
768        self.execute_after_edit(
769            ctx,
770            sketch,
771            sketch_block_ref,
772            segment_ids_edited,
773            EditDeleteKind::Edit,
774            &mut new_ast,
775        )
776        .await
777    }
778
779    async fn delete_objects(
780        &mut self,
781        ctx: &ExecutorContext,
782        _version: Version,
783        sketch: ObjectId,
784        constraint_ids: Vec<ObjectId>,
785        segment_ids: Vec<ObjectId>,
786    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
787        // TODO: Check version.
788        let sketch_block_ref =
789            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
790
791        // Deduplicate IDs.
792        let mut constraint_ids_set = constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
793        let segment_ids_set = segment_ids.into_iter().collect::<AhashIndexSet<_>>();
794
795        // If a point is owned by a Line/Arc, we want to delete the owner, which will
796        // also delete the point, as well as other points that are owned by the owner.
797        let mut resolved_segment_ids_to_delete = AhashIndexSet::default();
798
799        for segment_id in segment_ids_set.iter().copied() {
800            if let Some(segment_object) = self.scene_graph.objects.get(segment_id.0)
801                && let ObjectKind::Segment { segment } = &segment_object.kind
802                && let Segment::Point(point) = segment
803                && let Some(owner_id) = point.owner
804                && let Some(owner_object) = self.scene_graph.objects.get(owner_id.0)
805                && let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind
806                && matches!(owner_segment, Segment::Line(_) | Segment::Arc(_) | Segment::Circle(_))
807            {
808                // segment is owned -> delete the owner
809                resolved_segment_ids_to_delete.insert(owner_id);
810            } else {
811                // segment is not owned by anything -> can be deleted
812                resolved_segment_ids_to_delete.insert(segment_id);
813            }
814        }
815        let referenced_constraint_ids = self
816            .find_referenced_constraints(sketch, &resolved_segment_ids_to_delete)
817            .map_err(KclErrorWithOutputs::no_outputs)?;
818
819        let mut new_ast = self.program.ast.clone();
820
821        for constraint_id in referenced_constraint_ids {
822            if constraint_ids_set.contains(&constraint_id) {
823                continue;
824            }
825
826            let constraint_object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
827                KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Constraint not found: {constraint_id:?}")))
828            })?;
829            let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
830                return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
831                    "Object is not a constraint, it is {}",
832                    constraint_object.kind.human_friendly_kind_with_article()
833                ))));
834            };
835
836            match constraint {
837                Constraint::Coincident(coincident) => {
838                    let remaining_segments =
839                        self.remaining_constraint_segments(&coincident.segments, &resolved_segment_ids_to_delete);
840
841                    // If there are at least 2 segments left in the constraint: keep it, otherwise delete it.
842                    if remaining_segments.len() >= 2 {
843                        self.edit_coincident_constraint(&mut new_ast, constraint_id, remaining_segments)
844                            .map_err(KclErrorWithOutputs::no_outputs)?;
845                    } else {
846                        constraint_ids_set.insert(constraint_id);
847                    }
848                }
849                Constraint::EqualRadius(equal_radius) => {
850                    let remaining_input = equal_radius
851                        .input
852                        .iter()
853                        .copied()
854                        .filter(|segment_id| {
855                            !self.segment_will_be_deleted(*segment_id, &resolved_segment_ids_to_delete)
856                        })
857                        .collect::<Vec<_>>();
858
859                    if remaining_input.len() >= 2 {
860                        self.edit_equal_radius_constraint(&mut new_ast, constraint_id, remaining_input)
861                            .map_err(KclErrorWithOutputs::no_outputs)?;
862                    } else {
863                        constraint_ids_set.insert(constraint_id);
864                    }
865                }
866                Constraint::LinesEqualLength(lines_equal_length) => {
867                    let remaining_lines = lines_equal_length
868                        .lines
869                        .iter()
870                        .copied()
871                        .filter(|line_id| !self.segment_will_be_deleted(*line_id, &resolved_segment_ids_to_delete))
872                        .collect::<Vec<_>>();
873
874                    // Equal length constraint is only valid with at least 2 lines
875                    if remaining_lines.len() >= 2 {
876                        self.edit_equal_length_constraint(&mut new_ast, constraint_id, remaining_lines)
877                            .map_err(KclErrorWithOutputs::no_outputs)?;
878                    } else {
879                        constraint_ids_set.insert(constraint_id);
880                    }
881                }
882                Constraint::Parallel(parallel) => {
883                    let remaining_lines = parallel
884                        .lines
885                        .iter()
886                        .copied()
887                        .filter(|line_id| !self.segment_will_be_deleted(*line_id, &resolved_segment_ids_to_delete))
888                        .collect::<Vec<_>>();
889
890                    if remaining_lines.len() >= 2 {
891                        self.edit_parallel_constraint(&mut new_ast, constraint_id, remaining_lines)
892                            .map_err(KclErrorWithOutputs::no_outputs)?;
893                    } else {
894                        constraint_ids_set.insert(constraint_id);
895                    }
896                }
897                Constraint::Horizontal(Horizontal::Points { points }) => {
898                    let remaining_points = self.remaining_constraint_segments(points, &resolved_segment_ids_to_delete);
899
900                    if remaining_points.len() >= 2 {
901                        self.edit_horizontal_points_constraint(&mut new_ast, constraint_id, remaining_points)
902                            .map_err(KclErrorWithOutputs::no_outputs)?;
903                    } else {
904                        constraint_ids_set.insert(constraint_id);
905                    }
906                }
907                Constraint::Vertical(Vertical::Points { points }) => {
908                    let remaining_points = self.remaining_constraint_segments(points, &resolved_segment_ids_to_delete);
909
910                    if remaining_points.len() >= 2 {
911                        self.edit_vertical_points_constraint(&mut new_ast, constraint_id, remaining_points)
912                            .map_err(KclErrorWithOutputs::no_outputs)?;
913                    } else {
914                        constraint_ids_set.insert(constraint_id);
915                    }
916                }
917                Constraint::Fixed(fixed) => {
918                    if fixed.points.iter().any(|fixed_point| {
919                        self.segment_will_be_deleted(fixed_point.point, &resolved_segment_ids_to_delete)
920                    }) {
921                        constraint_ids_set.insert(constraint_id);
922                    }
923                }
924                _ => {
925                    // All other constraint types: if referenced by a segment -> delete the constraint
926                    constraint_ids_set.insert(constraint_id);
927                }
928            }
929        }
930
931        for constraint_id in constraint_ids_set {
932            self.delete_constraint(&mut new_ast, sketch, constraint_id)
933                .map_err(KclErrorWithOutputs::no_outputs)?;
934        }
935        for segment_id in resolved_segment_ids_to_delete {
936            self.delete_segment(&mut new_ast, sketch, segment_id)
937                .map_err(KclErrorWithOutputs::no_outputs)?;
938        }
939
940        self.execute_after_edit(
941            ctx,
942            sketch,
943            sketch_block_ref,
944            Default::default(),
945            EditDeleteKind::DeleteNonSketch,
946            &mut new_ast,
947        )
948        .await
949    }
950
951    async fn add_constraint(
952        &mut self,
953        ctx: &ExecutorContext,
954        _version: Version,
955        sketch: ObjectId,
956        constraint: Constraint,
957    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
958        // TODO: Check version.
959
960        // Save the original state as a backup - we'll restore it if anything fails
961        let original_program = self.program.clone();
962        let original_scene_graph = self.scene_graph.clone();
963
964        let mut new_ast = self.program.ast.clone();
965        let sketch_block_ref = match constraint {
966            Constraint::Coincident(coincident) => self
967                .add_coincident(sketch, coincident, &mut new_ast)
968                .await
969                .map_err(KclErrorWithOutputs::no_outputs)?,
970            Constraint::Distance(distance) => self
971                .add_distance(sketch, distance, &mut new_ast)
972                .await
973                .map_err(KclErrorWithOutputs::no_outputs)?,
974            Constraint::EqualRadius(equal_radius) => self
975                .add_equal_radius(sketch, equal_radius, &mut new_ast)
976                .await
977                .map_err(KclErrorWithOutputs::no_outputs)?,
978            Constraint::Fixed(fixed) => self
979                .add_fixed_constraints(sketch, fixed.points, &mut new_ast)
980                .await
981                .map_err(KclErrorWithOutputs::no_outputs)?,
982            Constraint::HorizontalDistance(distance) => self
983                .add_horizontal_distance(sketch, distance, &mut new_ast)
984                .await
985                .map_err(KclErrorWithOutputs::no_outputs)?,
986            Constraint::VerticalDistance(distance) => self
987                .add_vertical_distance(sketch, distance, &mut new_ast)
988                .await
989                .map_err(KclErrorWithOutputs::no_outputs)?,
990            Constraint::Horizontal(horizontal) => self
991                .add_horizontal(sketch, horizontal, &mut new_ast)
992                .await
993                .map_err(KclErrorWithOutputs::no_outputs)?,
994            Constraint::LinesEqualLength(lines_equal_length) => self
995                .add_lines_equal_length(sketch, lines_equal_length, &mut new_ast)
996                .await
997                .map_err(KclErrorWithOutputs::no_outputs)?,
998            Constraint::Midpoint(midpoint) => self
999                .add_midpoint(sketch, midpoint, &mut new_ast)
1000                .await
1001                .map_err(KclErrorWithOutputs::no_outputs)?,
1002            Constraint::Parallel(parallel) => self
1003                .add_parallel(sketch, parallel, &mut new_ast)
1004                .await
1005                .map_err(KclErrorWithOutputs::no_outputs)?,
1006            Constraint::Perpendicular(perpendicular) => self
1007                .add_perpendicular(sketch, perpendicular, &mut new_ast)
1008                .await
1009                .map_err(KclErrorWithOutputs::no_outputs)?,
1010            Constraint::Radius(radius) => self
1011                .add_radius(sketch, radius, &mut new_ast)
1012                .await
1013                .map_err(KclErrorWithOutputs::no_outputs)?,
1014            Constraint::Diameter(diameter) => self
1015                .add_diameter(sketch, diameter, &mut new_ast)
1016                .await
1017                .map_err(KclErrorWithOutputs::no_outputs)?,
1018            Constraint::Symmetric(symmetric) => self
1019                .add_symmetric(sketch, symmetric, &mut new_ast)
1020                .await
1021                .map_err(KclErrorWithOutputs::no_outputs)?,
1022            Constraint::Vertical(vertical) => self
1023                .add_vertical(sketch, vertical, &mut new_ast)
1024                .await
1025                .map_err(KclErrorWithOutputs::no_outputs)?,
1026            Constraint::Angle(lines_at_angle) => self
1027                .add_angle(sketch, lines_at_angle, &mut new_ast)
1028                .await
1029                .map_err(KclErrorWithOutputs::no_outputs)?,
1030            Constraint::Tangent(tangent) => self
1031                .add_tangent(sketch, tangent, &mut new_ast)
1032                .await
1033                .map_err(KclErrorWithOutputs::no_outputs)?,
1034        };
1035
1036        let result = self
1037            .execute_after_add_constraint(ctx, sketch, sketch_block_ref, &mut new_ast)
1038            .await;
1039
1040        // If execution failed, restore the original state to prevent corruption
1041        if result.is_err() {
1042            self.program = original_program;
1043            self.scene_graph = original_scene_graph;
1044        }
1045
1046        result
1047    }
1048
1049    async fn chain_segment(
1050        &mut self,
1051        ctx: &ExecutorContext,
1052        version: Version,
1053        sketch: ObjectId,
1054        previous_segment_end_point_id: ObjectId,
1055        segment: SegmentCtor,
1056        _label: Option<String>,
1057    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1058        // TODO: Check version.
1059
1060        // First, add the segment (line) to get its start point ID
1061        let SegmentCtor::Line(line_ctor) = segment else {
1062            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1063                "chain_segment currently only supports Line segments, got {}",
1064                segment.human_friendly_kind_with_article(),
1065            ))));
1066        };
1067
1068        // Add the line segment first - this updates self.program and self.scene_graph
1069        let (_first_src_delta, first_scene_delta) = self.add_line(ctx, sketch, line_ctor).await?;
1070
1071        // Find the new line's start point ID from the updated scene graph
1072        // add_line updates self.scene_graph, so we can use that
1073        let new_line_id = first_scene_delta
1074            .new_objects
1075            .iter()
1076            .find(|&obj_id| {
1077                let obj = self.scene_graph.objects.get(obj_id.0);
1078                if let Some(obj) = obj {
1079                    matches!(
1080                        &obj.kind,
1081                        ObjectKind::Segment {
1082                            segment: Segment::Line(_)
1083                        }
1084                    )
1085                } else {
1086                    false
1087                }
1088            })
1089            .ok_or_else(|| {
1090                KclErrorWithOutputs::no_outputs(KclError::refactor(
1091                    "Failed to find new line segment in scene graph".to_string(),
1092                ))
1093            })?;
1094
1095        let new_line_obj = self.scene_graph.objects.get(new_line_id.0).ok_or_else(|| {
1096            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1097                "New line object not found: {new_line_id:?}"
1098            )))
1099        })?;
1100
1101        let ObjectKind::Segment {
1102            segment: new_line_segment,
1103        } = &new_line_obj.kind
1104        else {
1105            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1106                "Object is not a segment: {new_line_obj:?}"
1107            ))));
1108        };
1109
1110        let Segment::Line(new_line) = new_line_segment else {
1111            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1112                "Segment is not a line: {new_line_segment:?}"
1113            ))));
1114        };
1115
1116        let new_line_start_point_id = new_line.start;
1117
1118        // Now add the coincident constraint between the previous end point and the new line's start point.
1119        let coincident = Coincident {
1120            segments: vec![previous_segment_end_point_id.into(), new_line_start_point_id.into()],
1121        };
1122
1123        let (final_src_delta, final_scene_delta) = self
1124            .add_constraint(ctx, version, sketch, Constraint::Coincident(coincident))
1125            .await?;
1126
1127        // Combine new objects from the line addition and the constraint addition.
1128        // Both add_line and add_constraint now populate new_objects correctly.
1129        let mut combined_new_objects = first_scene_delta.new_objects.clone();
1130        combined_new_objects.extend(final_scene_delta.new_objects);
1131
1132        let scene_graph_delta = SceneGraphDelta {
1133            new_graph: self.scene_graph.clone(),
1134            invalidates_ids: false,
1135            new_objects: combined_new_objects,
1136            exec_outcome: final_scene_delta.exec_outcome,
1137        };
1138
1139        Ok((final_src_delta, scene_graph_delta))
1140    }
1141
1142    async fn edit_constraint(
1143        &mut self,
1144        ctx: &ExecutorContext,
1145        _version: Version,
1146        sketch: ObjectId,
1147        constraint_id: ObjectId,
1148        value_expression: String,
1149    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1150        // TODO: Check version.
1151        let sketch_block_ref =
1152            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1153
1154        let object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
1155            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Object not found: {constraint_id:?}")))
1156        })?;
1157        if !matches!(&object.kind, ObjectKind::Constraint { .. }) {
1158            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1159                "Object is not a constraint: {constraint_id:?}"
1160            ))));
1161        }
1162
1163        let mut new_ast = self.program.ast.clone();
1164
1165        // Parse the expression string into an AST node.
1166        let (parsed, errors) = Program::parse(&value_expression)
1167            .map_err(|e| KclErrorWithOutputs::no_outputs(KclError::refactor(e.to_string())))?;
1168        if !errors.is_empty() {
1169            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1170                "Error parsing value expression: {errors:?}"
1171            ))));
1172        }
1173        let mut parsed = parsed.ok_or_else(|| {
1174            KclErrorWithOutputs::no_outputs(KclError::refactor("No AST produced from value expression".to_string()))
1175        })?;
1176        if parsed.ast.body.is_empty() {
1177            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1178                "Empty value expression".to_string(),
1179            )));
1180        }
1181        let first = parsed.ast.body.remove(0);
1182        let ast::BodyItem::ExpressionStatement(expr_stmt) = first else {
1183            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1184                "Value expression must be a simple expression".to_string(),
1185            )));
1186        };
1187
1188        let new_value: ast::BinaryPart = expr_stmt
1189            .inner
1190            .expression
1191            .try_into()
1192            .map_err(|e: String| KclErrorWithOutputs::no_outputs(KclError::refactor(e)))?;
1193
1194        self.mutate_ast(
1195            &mut new_ast,
1196            constraint_id,
1197            AstMutateCommand::EditConstraintValue { value: new_value },
1198        )
1199        .map_err(KclErrorWithOutputs::no_outputs)?;
1200
1201        self.execute_after_edit(
1202            ctx,
1203            sketch,
1204            sketch_block_ref,
1205            Default::default(),
1206            EditDeleteKind::Edit,
1207            &mut new_ast,
1208        )
1209        .await
1210    }
1211
1212    async fn edit_distance_constraint_label_position(
1213        &mut self,
1214        ctx: &ExecutorContext,
1215        _version: Version,
1216        sketch: ObjectId,
1217        constraint_id: ObjectId,
1218        label_position: Point2d<Number>,
1219        anchor_segment_ids: Vec<ObjectId>,
1220    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1221        // TODO: Check version.
1222        let sketch_block_ref =
1223            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1224
1225        let object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
1226            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Object not found: {constraint_id:?}")))
1227        })?;
1228        if !matches!(
1229            &object.kind,
1230            ObjectKind::Constraint {
1231                constraint: Constraint::Distance(_)
1232                    | Constraint::HorizontalDistance(_)
1233                    | Constraint::VerticalDistance(_)
1234                    | Constraint::Radius(_)
1235                    | Constraint::Diameter(_),
1236            }
1237        ) {
1238            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1239                "Object does not support labelPosition: {constraint_id:?}"
1240            ))));
1241        }
1242
1243        let label_position = to_ast_point2d_number(&label_position).map_err(|err| {
1244            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1245                "Could not convert label position to AST: {err}"
1246            )))
1247        })?;
1248        let mut new_ast = self.program.ast.clone();
1249        self.mutate_ast(
1250            &mut new_ast,
1251            constraint_id,
1252            AstMutateCommand::EditDistanceConstraintLabelPosition { label_position },
1253        )
1254        .map_err(KclErrorWithOutputs::no_outputs)?;
1255
1256        self.execute_after_edit(
1257            ctx,
1258            sketch,
1259            sketch_block_ref,
1260            anchor_segment_ids.into_iter().collect(),
1261            EditDeleteKind::Edit,
1262            &mut new_ast,
1263        )
1264        .await
1265    }
1266
1267    /// Splitting a segment means creating a new segment, editing the old one, and then
1268    /// migrating a bunch of the constraints from the original segment to the new one
1269    /// (i.e. deleting them and re-adding them on the other segment).
1270    ///
1271    /// To keep this efficient we require as few executions as possible: we create the
1272    /// new segment first (to get its id), then do all edits and new constraints, and
1273    /// do all deletes at the end (since deletes invalidate ids).
1274    async fn batch_split_segment_operations(
1275        &mut self,
1276        ctx: &ExecutorContext,
1277        _version: Version,
1278        sketch: ObjectId,
1279        edit_segments: Vec<ExistingSegmentCtor>,
1280        add_constraints: Vec<Constraint>,
1281        delete_constraint_ids: Vec<ObjectId>,
1282        _new_segment_info: sketch::NewSegmentInfo,
1283    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1284        // TODO: Check version.
1285        let sketch_block_ref =
1286            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1287
1288        let mut new_ast = self.program.ast.clone();
1289        let mut segment_ids_edited = AhashIndexSet::with_capacity_and_hasher(edit_segments.len(), Default::default());
1290
1291        // Step 1: Edit segments
1292        for segment in edit_segments {
1293            segment_ids_edited.insert(segment.id);
1294            match segment.ctor {
1295                SegmentCtor::Point(ctor) => self
1296                    .edit_point(&mut new_ast, sketch, segment.id, ctor)
1297                    .map_err(KclErrorWithOutputs::no_outputs)?,
1298                SegmentCtor::Line(ctor) => self
1299                    .edit_line(&mut new_ast, sketch, segment.id, ctor)
1300                    .map_err(KclErrorWithOutputs::no_outputs)?,
1301                SegmentCtor::Arc(ctor) => self
1302                    .edit_arc(&mut new_ast, sketch, segment.id, ctor)
1303                    .map_err(KclErrorWithOutputs::no_outputs)?,
1304                SegmentCtor::Circle(ctor) => self
1305                    .edit_circle(&mut new_ast, sketch, segment.id, ctor)
1306                    .map_err(KclErrorWithOutputs::no_outputs)?,
1307            }
1308        }
1309
1310        // Step 2: Add all constraints
1311        for constraint in add_constraints {
1312            match constraint {
1313                Constraint::Coincident(coincident) => {
1314                    self.add_coincident(sketch, coincident, &mut new_ast)
1315                        .await
1316                        .map_err(KclErrorWithOutputs::no_outputs)?;
1317                }
1318                Constraint::Distance(distance) => {
1319                    self.add_distance(sketch, distance, &mut new_ast)
1320                        .await
1321                        .map_err(KclErrorWithOutputs::no_outputs)?;
1322                }
1323                Constraint::EqualRadius(equal_radius) => {
1324                    self.add_equal_radius(sketch, equal_radius, &mut new_ast)
1325                        .await
1326                        .map_err(KclErrorWithOutputs::no_outputs)?;
1327                }
1328                Constraint::Fixed(fixed) => {
1329                    self.add_fixed_constraints(sketch, fixed.points, &mut new_ast)
1330                        .await
1331                        .map_err(KclErrorWithOutputs::no_outputs)?;
1332                }
1333                Constraint::HorizontalDistance(distance) => {
1334                    self.add_horizontal_distance(sketch, distance, &mut new_ast)
1335                        .await
1336                        .map_err(KclErrorWithOutputs::no_outputs)?;
1337                }
1338                Constraint::VerticalDistance(distance) => {
1339                    self.add_vertical_distance(sketch, distance, &mut new_ast)
1340                        .await
1341                        .map_err(KclErrorWithOutputs::no_outputs)?;
1342                }
1343                Constraint::Horizontal(horizontal) => {
1344                    self.add_horizontal(sketch, horizontal, &mut new_ast)
1345                        .await
1346                        .map_err(KclErrorWithOutputs::no_outputs)?;
1347                }
1348                Constraint::LinesEqualLength(lines_equal_length) => {
1349                    self.add_lines_equal_length(sketch, lines_equal_length, &mut new_ast)
1350                        .await
1351                        .map_err(KclErrorWithOutputs::no_outputs)?;
1352                }
1353                Constraint::Midpoint(midpoint) => {
1354                    self.add_midpoint(sketch, midpoint, &mut new_ast)
1355                        .await
1356                        .map_err(KclErrorWithOutputs::no_outputs)?;
1357                }
1358                Constraint::Parallel(parallel) => {
1359                    self.add_parallel(sketch, parallel, &mut new_ast)
1360                        .await
1361                        .map_err(KclErrorWithOutputs::no_outputs)?;
1362                }
1363                Constraint::Perpendicular(perpendicular) => {
1364                    self.add_perpendicular(sketch, perpendicular, &mut new_ast)
1365                        .await
1366                        .map_err(KclErrorWithOutputs::no_outputs)?;
1367                }
1368                Constraint::Vertical(vertical) => {
1369                    self.add_vertical(sketch, vertical, &mut new_ast)
1370                        .await
1371                        .map_err(KclErrorWithOutputs::no_outputs)?;
1372                }
1373                Constraint::Diameter(diameter) => {
1374                    self.add_diameter(sketch, diameter, &mut new_ast)
1375                        .await
1376                        .map_err(KclErrorWithOutputs::no_outputs)?;
1377                }
1378                Constraint::Radius(radius) => {
1379                    self.add_radius(sketch, radius, &mut new_ast)
1380                        .await
1381                        .map_err(KclErrorWithOutputs::no_outputs)?;
1382                }
1383                Constraint::Symmetric(symmetric) => {
1384                    self.add_symmetric(sketch, symmetric, &mut new_ast)
1385                        .await
1386                        .map_err(KclErrorWithOutputs::no_outputs)?;
1387                }
1388                Constraint::Angle(angle) => {
1389                    self.add_angle(sketch, angle, &mut new_ast)
1390                        .await
1391                        .map_err(KclErrorWithOutputs::no_outputs)?;
1392                }
1393                Constraint::Tangent(tangent) => {
1394                    self.add_tangent(sketch, tangent, &mut new_ast)
1395                        .await
1396                        .map_err(KclErrorWithOutputs::no_outputs)?;
1397                }
1398            }
1399        }
1400
1401        // Step 3: Delete constraints (must be last since deletes can invalidate IDs)
1402        let constraint_ids_set = delete_constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
1403
1404        let has_constraint_deletions = !constraint_ids_set.is_empty();
1405        for constraint_id in constraint_ids_set {
1406            self.delete_constraint(&mut new_ast, sketch, constraint_id)
1407                .map_err(KclErrorWithOutputs::no_outputs)?;
1408        }
1409
1410        // Step 4: Execute once at the end
1411        // Always use Edit (not DeleteNonSketch) because we're editing the sketch block, not deleting it
1412        // But we'll manually set invalidates_ids: true if we deleted constraints
1413        let (source_delta, mut scene_graph_delta) = self
1414            .execute_after_edit(
1415                ctx,
1416                sketch,
1417                sketch_block_ref,
1418                segment_ids_edited,
1419                EditDeleteKind::Edit,
1420                &mut new_ast,
1421            )
1422            .await?;
1423
1424        // If we deleted constraints, set invalidates_ids: true
1425        // This is because constraint deletion invalidates IDs, even though we're not deleting the sketch block
1426        if has_constraint_deletions {
1427            scene_graph_delta.invalidates_ids = true;
1428        }
1429
1430        Ok((source_delta, scene_graph_delta))
1431    }
1432
1433    async fn batch_tail_cut_operations(
1434        &mut self,
1435        ctx: &ExecutorContext,
1436        _version: Version,
1437        sketch: ObjectId,
1438        edit_segments: Vec<ExistingSegmentCtor>,
1439        add_constraints: Vec<Constraint>,
1440        delete_constraint_ids: Vec<ObjectId>,
1441        additional_edited_segment_ids: Vec<ObjectId>,
1442    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1443        let sketch_block_ref =
1444            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1445
1446        let mut new_ast = self.program.ast.clone();
1447        let mut segment_ids_edited = AhashIndexSet::with_capacity_and_hasher(edit_segments.len(), Default::default());
1448
1449        // Step 1: Edit segments (usually a single segment for tail cut)
1450        for segment in edit_segments {
1451            segment_ids_edited.insert(segment.id);
1452            match segment.ctor {
1453                SegmentCtor::Point(ctor) => self
1454                    .edit_point(&mut new_ast, sketch, segment.id, ctor)
1455                    .map_err(KclErrorWithOutputs::no_outputs)?,
1456                SegmentCtor::Line(ctor) => self
1457                    .edit_line(&mut new_ast, sketch, segment.id, ctor)
1458                    .map_err(KclErrorWithOutputs::no_outputs)?,
1459                SegmentCtor::Arc(ctor) => self
1460                    .edit_arc(&mut new_ast, sketch, segment.id, ctor)
1461                    .map_err(KclErrorWithOutputs::no_outputs)?,
1462                SegmentCtor::Circle(ctor) => self
1463                    .edit_circle(&mut new_ast, sketch, segment.id, ctor)
1464                    .map_err(KclErrorWithOutputs::no_outputs)?,
1465            }
1466        }
1467
1468        segment_ids_edited.extend(additional_edited_segment_ids);
1469
1470        // Step 2: Add coincident constraints
1471        for constraint in add_constraints {
1472            match constraint {
1473                Constraint::Coincident(coincident) => {
1474                    self.add_coincident(sketch, coincident, &mut new_ast)
1475                        .await
1476                        .map_err(KclErrorWithOutputs::no_outputs)?;
1477                }
1478                other => {
1479                    return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1480                        "unsupported constraint in tail cut batch: {other:?}"
1481                    ))));
1482                }
1483            }
1484        }
1485
1486        // Step 3: Delete constraints (if any)
1487        let constraint_ids_set = delete_constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
1488
1489        let has_constraint_deletions = !constraint_ids_set.is_empty();
1490        for constraint_id in constraint_ids_set {
1491            self.delete_constraint(&mut new_ast, sketch, constraint_id)
1492                .map_err(KclErrorWithOutputs::no_outputs)?;
1493        }
1494
1495        // Step 4: Single execute_after_edit
1496        // Always use Edit (not DeleteNonSketch) because we're editing the sketch block, not deleting it
1497        // But we'll manually set invalidates_ids: true if we deleted constraints
1498        let (source_delta, mut scene_graph_delta) = self
1499            .execute_after_edit(
1500                ctx,
1501                sketch,
1502                sketch_block_ref,
1503                segment_ids_edited,
1504                EditDeleteKind::Edit,
1505                &mut new_ast,
1506            )
1507            .await?;
1508
1509        // If we deleted constraints, set invalidates_ids: true
1510        // This is because constraint deletion invalidates IDs, even though we're not deleting the sketch block
1511        if has_constraint_deletions {
1512            scene_graph_delta.invalidates_ids = true;
1513        }
1514
1515        Ok((source_delta, scene_graph_delta))
1516    }
1517}
1518
1519impl FrontendState {
1520    pub async fn hack_set_program(&mut self, ctx: &ExecutorContext, program: Program) -> ExecResult<SetProgramOutcome> {
1521        self.program = program.clone();
1522
1523        // Execute so that the objects are updated and available for the next
1524        // API call.
1525        // This always uses engine execution (not mock) so that things are cached.
1526        // Engine execution now runs freedom analysis automatically.
1527        // Keep existing checkpoints alive here. History may still reference
1528        // older committed sketch states across a direct-edit boundary, and a
1529        // checkpoint restore is a full state replacement anyway. We append a
1530        // fresh baseline checkpoint after the full execution below.
1531        // Clear the freedom cache since IDs might have changed after direct editing
1532        // and we're about to run freedom analysis which will repopulate it.
1533        self.point_freedom_cache.clear();
1534        match ctx.run_with_caching(program).await {
1535            Ok(outcome) => {
1536                let outcome = self.update_state_after_exec(outcome, true);
1537                let checkpoint_id = self
1538                    .create_sketch_checkpoint(outcome.clone())
1539                    .await
1540                    .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
1541                Ok(SetProgramOutcome::Success {
1542                    scene_graph: Box::new(self.scene_graph.clone()),
1543                    exec_outcome: Box::new(outcome),
1544                    checkpoint_id: Some(checkpoint_id),
1545                })
1546            }
1547            Err(mut err) => {
1548                // Don't return an error just because execution failed. Instead,
1549                // update state as much as possible.
1550                let outcome = self.exec_outcome_from_exec_error(err.clone())?;
1551                self.update_state_after_exec(outcome, true);
1552                err.scene_graph = Some(self.scene_graph.clone());
1553                Ok(SetProgramOutcome::ExecFailure { error: Box::new(err) })
1554            }
1555        }
1556    }
1557
1558    /// Decorate engine execution such that our state is updated and the scene
1559    /// graph is added to the return.
1560    pub async fn engine_execute(
1561        &mut self,
1562        ctx: &ExecutorContext,
1563        program: Program,
1564    ) -> Result<SceneGraphDelta, KclErrorWithOutputs> {
1565        self.program = program.clone();
1566
1567        // Engine execution now runs freedom analysis automatically. Clear the
1568        // freedom cache since IDs might have changed after direct editing, and
1569        // we're about to run freedom analysis which will repopulate it.
1570        self.point_freedom_cache.clear();
1571        match ctx.run_with_caching(program).await {
1572            Ok(outcome) => {
1573                let outcome = self.update_state_after_exec(outcome, true);
1574                Ok(SceneGraphDelta {
1575                    new_graph: self.scene_graph.clone(),
1576                    exec_outcome: outcome,
1577                    // We don't know what the new objects are.
1578                    new_objects: Default::default(),
1579                    // We don't know if IDs were invalidated.
1580                    invalidates_ids: Default::default(),
1581                })
1582            }
1583            Err(mut err) => {
1584                // Update state as much as possible, even when there's an error.
1585                let outcome = self.exec_outcome_from_exec_error(err.clone())?;
1586                self.update_state_after_exec(outcome, true);
1587                err.scene_graph = Some(self.scene_graph.clone());
1588                Err(err)
1589            }
1590        }
1591    }
1592
1593    fn exec_outcome_from_exec_error(&self, err: KclErrorWithOutputs) -> Result<ExecOutcome, KclErrorWithOutputs> {
1594        if matches!(err.error, KclError::EngineHangup { .. }) {
1595            // It's not ideal to special-case this, but this error is very
1596            // common during development, and it causes confusing downstream
1597            // errors that have nothing to do with the actual problem.
1598            return Err(err);
1599        }
1600
1601        let KclErrorWithOutputs {
1602            error,
1603            mut non_fatal,
1604            variables,
1605            operations,
1606            artifact_graph,
1607            scene_objects,
1608            source_range_to_object,
1609            var_solutions,
1610            filenames,
1611            default_planes,
1612            ..
1613        } = err;
1614
1615        if let Some(source_range) = error.source_ranges().first() {
1616            non_fatal.push(CompilationIssue::fatal(*source_range, error.get_message()));
1617        } else {
1618            non_fatal.push(CompilationIssue::fatal(SourceRange::synthetic(), error.get_message()));
1619        }
1620
1621        Ok(ExecOutcome {
1622            variables,
1623            filenames,
1624            operations,
1625            artifact_graph,
1626            scene_objects,
1627            source_range_to_object,
1628            var_solutions,
1629            issues: non_fatal,
1630            default_planes,
1631        })
1632    }
1633
1634    async fn add_point(
1635        &mut self,
1636        ctx: &ExecutorContext,
1637        sketch: ObjectId,
1638        ctor: PointCtor,
1639    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1640        // Create updated KCL source from args.
1641        let at_ast = to_ast_point2d(&ctor.position)
1642            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1643        let point_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
1644            callee: ast::Node::no_src(ast_sketch2_name(POINT_FN)),
1645            unlabeled: None,
1646            arguments: vec![ast::LabeledArg {
1647                label: Some(ast::Identifier::new(POINT_AT_PARAM)),
1648                arg: at_ast,
1649            }],
1650            digest: None,
1651            non_code_meta: Default::default(),
1652        })));
1653
1654        // Look up existing sketch.
1655        let sketch_id = sketch;
1656        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
1657            #[cfg(target_arch = "wasm32")]
1658            web_sys::console::error_1(
1659                &format!(
1660                    "Sketch not found; sketch_id={sketch_id:?}, self.scene_graph.objects={:#?}",
1661                    &self.scene_graph.objects
1662                )
1663                .into(),
1664            );
1665            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
1666        })?;
1667        let ObjectKind::Sketch(_) = &sketch_object.kind else {
1668            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1669                "Object is not a sketch, it is {}",
1670                sketch_object.kind.human_friendly_kind_with_article(),
1671            ))));
1672        };
1673        // Add the point to the AST of the sketch block.
1674        let mut new_ast = self.program.ast.clone();
1675        let (sketch_block_ref, _) = self
1676            .mutate_ast(
1677                &mut new_ast,
1678                sketch_id,
1679                AstMutateCommand::AddSketchBlockExprStmt { expr: point_ast },
1680            )
1681            .map_err(KclErrorWithOutputs::no_outputs)?;
1682        // Convert to string source to create real source ranges.
1683        let new_source = source_from_ast(&new_ast);
1684        // Parse the new KCL source.
1685        let (new_program, errors) = Program::parse(&new_source)
1686            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1687        if !errors.is_empty() {
1688            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1689                "Error parsing KCL source after adding point: {errors:?}"
1690            ))));
1691        }
1692        let Some(new_program) = new_program else {
1693            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1694                "No AST produced after adding point".to_string(),
1695            )));
1696        };
1697
1698        let point_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
1699            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1700                "Source range of point not found in sketch block: {sketch_block_ref:?}; {err:?}"
1701            )))
1702        })?;
1703
1704        // Make sure to only set this if there are no errors.
1705        self.program = new_program.clone();
1706
1707        // Truncate after the sketch block for mock execution.
1708        let mut truncated_program = new_program;
1709        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
1710            .map_err(KclErrorWithOutputs::no_outputs)?;
1711
1712        // Execute.
1713        let outcome = ctx
1714            .run_mock(
1715                &truncated_program,
1716                &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
1717            )
1718            .await?;
1719
1720        let new_object_ids = {
1721            let make_err =
1722                |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
1723            let segment_id = outcome
1724                .source_range_to_object
1725                .get(&point_node_ref.range)
1726                .copied()
1727                .ok_or_else(|| make_err(format!("Source range of point not found: {point_node_ref:?}")))?;
1728            let segment_object = outcome
1729                .scene_objects
1730                .get(segment_id.0)
1731                .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
1732            let ObjectKind::Segment { segment } = &segment_object.kind else {
1733                return Err(make_err(format!(
1734                    "Object is not a segment, it is {}",
1735                    segment_object.kind.human_friendly_kind_with_article()
1736                )));
1737            };
1738            let Segment::Point(_) = segment else {
1739                return Err(make_err(format!(
1740                    "Segment is not a point, it is {}",
1741                    segment.human_friendly_kind_with_article()
1742                )));
1743            };
1744            vec![segment_id]
1745        };
1746        let src_delta = SourceDelta { text: new_source };
1747        // Uses .no_freedom_analysis() so freedom_analysis: false
1748        let outcome = self.update_state_after_exec(outcome, false);
1749        let scene_graph_delta = SceneGraphDelta {
1750            new_graph: self.scene_graph.clone(),
1751            invalidates_ids: false,
1752            new_objects: new_object_ids,
1753            exec_outcome: outcome,
1754        };
1755        Ok((src_delta, scene_graph_delta))
1756    }
1757
1758    async fn add_line(
1759        &mut self,
1760        ctx: &ExecutorContext,
1761        sketch: ObjectId,
1762        ctor: LineCtor,
1763    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1764        // Create updated KCL source from args.
1765        let start_ast = to_ast_point2d(&ctor.start)
1766            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1767        let end_ast = to_ast_point2d(&ctor.end)
1768            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1769        let mut arguments = vec![
1770            ast::LabeledArg {
1771                label: Some(ast::Identifier::new(LINE_START_PARAM)),
1772                arg: start_ast,
1773            },
1774            ast::LabeledArg {
1775                label: Some(ast::Identifier::new(LINE_END_PARAM)),
1776                arg: end_ast,
1777            },
1778        ];
1779        // Add construction kwarg if construction is Some(true)
1780        if ctor.construction == Some(true) {
1781            arguments.push(ast::LabeledArg {
1782                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
1783                arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
1784                    value: ast::LiteralValue::Bool(true),
1785                    raw: "true".to_string(),
1786                    digest: None,
1787                }))),
1788            });
1789        }
1790        let line_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
1791            callee: ast::Node::no_src(ast_sketch2_name(LINE_FN)),
1792            unlabeled: None,
1793            arguments,
1794            digest: None,
1795            non_code_meta: Default::default(),
1796        })));
1797
1798        // Look up existing sketch.
1799        let sketch_id = sketch;
1800        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
1801            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
1802        })?;
1803        let ObjectKind::Sketch(_) = &sketch_object.kind else {
1804            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1805                "Object is not a sketch, it is {}",
1806                sketch_object.kind.human_friendly_kind_with_article(),
1807            ))));
1808        };
1809        // Add the line to the AST of the sketch block.
1810        let mut new_ast = self.program.ast.clone();
1811        let (sketch_block_ref, _) = self
1812            .mutate_ast(
1813                &mut new_ast,
1814                sketch_id,
1815                AstMutateCommand::AddSketchBlockExprStmt { expr: line_ast },
1816            )
1817            .map_err(KclErrorWithOutputs::no_outputs)?;
1818        // Convert to string source to create real source ranges.
1819        let new_source = source_from_ast(&new_ast);
1820        // Parse the new KCL source.
1821        let (new_program, errors) = Program::parse(&new_source)
1822            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1823        if !errors.is_empty() {
1824            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1825                "Error parsing KCL source after adding line: {errors:?}"
1826            ))));
1827        }
1828        let Some(new_program) = new_program else {
1829            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1830                "No AST produced after adding line".to_string(),
1831            )));
1832        };
1833
1834        let line_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
1835            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1836                "Source range of line not found in sketch block: {sketch_block_ref:?}; {err:?}"
1837            )))
1838        })?;
1839
1840        // Make sure to only set this if there are no errors.
1841        self.program = new_program.clone();
1842
1843        // Truncate after the sketch block for mock execution.
1844        let mut truncated_program = new_program;
1845        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
1846            .map_err(KclErrorWithOutputs::no_outputs)?;
1847
1848        // Execute.
1849        let outcome = ctx
1850            .run_mock(
1851                &truncated_program,
1852                &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
1853            )
1854            .await?;
1855
1856        let new_object_ids = {
1857            let make_err =
1858                |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
1859            let segment_id = outcome
1860                .source_range_to_object
1861                .get(&line_node_ref.range)
1862                .copied()
1863                .ok_or_else(|| make_err(format!("Source range of line not found: {line_node_ref:?}")))?;
1864            let segment_object = outcome
1865                .scene_object_by_id(segment_id)
1866                .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
1867            let ObjectKind::Segment { segment } = &segment_object.kind else {
1868                return Err(make_err(format!(
1869                    "Object is not a segment, it is {}",
1870                    segment_object.kind.human_friendly_kind_with_article()
1871                )));
1872            };
1873            let Segment::Line(line) = segment else {
1874                return Err(make_err(format!(
1875                    "Segment is not a line, it is {}",
1876                    segment.human_friendly_kind_with_article()
1877                )));
1878            };
1879            vec![line.start, line.end, segment_id]
1880        };
1881        let src_delta = SourceDelta { text: new_source };
1882        // Uses .no_freedom_analysis() so freedom_analysis: false
1883        let outcome = self.update_state_after_exec(outcome, false);
1884        let scene_graph_delta = SceneGraphDelta {
1885            new_graph: self.scene_graph.clone(),
1886            invalidates_ids: false,
1887            new_objects: new_object_ids,
1888            exec_outcome: outcome,
1889        };
1890        Ok((src_delta, scene_graph_delta))
1891    }
1892
1893    async fn add_arc(
1894        &mut self,
1895        ctx: &ExecutorContext,
1896        sketch: ObjectId,
1897        ctor: ArcCtor,
1898    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1899        // Create updated KCL source from args.
1900        let start_ast = to_ast_point2d(&ctor.start)
1901            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1902        let end_ast = to_ast_point2d(&ctor.end)
1903            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1904        let center_ast = to_ast_point2d(&ctor.center)
1905            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1906        let mut arguments = vec![
1907            ast::LabeledArg {
1908                label: Some(ast::Identifier::new(ARC_START_PARAM)),
1909                arg: start_ast,
1910            },
1911            ast::LabeledArg {
1912                label: Some(ast::Identifier::new(ARC_END_PARAM)),
1913                arg: end_ast,
1914            },
1915            ast::LabeledArg {
1916                label: Some(ast::Identifier::new(ARC_CENTER_PARAM)),
1917                arg: center_ast,
1918            },
1919        ];
1920        // Add construction kwarg if construction is Some(true)
1921        if ctor.construction == Some(true) {
1922            arguments.push(ast::LabeledArg {
1923                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
1924                arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
1925                    value: ast::LiteralValue::Bool(true),
1926                    raw: "true".to_string(),
1927                    digest: None,
1928                }))),
1929            });
1930        }
1931        let arc_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
1932            callee: ast::Node::no_src(ast_sketch2_name(ARC_FN)),
1933            unlabeled: None,
1934            arguments,
1935            digest: None,
1936            non_code_meta: Default::default(),
1937        })));
1938
1939        // Look up existing sketch.
1940        let sketch_id = sketch;
1941        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
1942            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
1943        })?;
1944        let ObjectKind::Sketch(_) = &sketch_object.kind else {
1945            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1946                "Object is not a sketch, it is {}",
1947                sketch_object.kind.human_friendly_kind_with_article(),
1948            ))));
1949        };
1950        // Add the arc to the AST of the sketch block.
1951        let mut new_ast = self.program.ast.clone();
1952        let (sketch_block_ref, _) = self
1953            .mutate_ast(
1954                &mut new_ast,
1955                sketch_id,
1956                AstMutateCommand::AddSketchBlockExprStmt { expr: arc_ast },
1957            )
1958            .map_err(KclErrorWithOutputs::no_outputs)?;
1959        // Convert to string source to create real source ranges.
1960        let new_source = source_from_ast(&new_ast);
1961        // Parse the new KCL source.
1962        let (new_program, errors) = Program::parse(&new_source)
1963            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1964        if !errors.is_empty() {
1965            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1966                "Error parsing KCL source after adding arc: {errors:?}"
1967            ))));
1968        }
1969        let Some(new_program) = new_program else {
1970            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1971                "No AST produced after adding arc".to_string(),
1972            )));
1973        };
1974
1975        let arc_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
1976            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1977                "Source range of arc not found in sketch block: {sketch_block_ref:?}; {err:?}"
1978            )))
1979        })?;
1980
1981        // Make sure to only set this if there are no errors.
1982        self.program = new_program.clone();
1983
1984        // Truncate after the sketch block for mock execution.
1985        let mut truncated_program = new_program;
1986        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
1987            .map_err(KclErrorWithOutputs::no_outputs)?;
1988
1989        // Execute.
1990        let outcome = ctx
1991            .run_mock(
1992                &truncated_program,
1993                &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
1994            )
1995            .await?;
1996
1997        let new_object_ids = {
1998            let make_err =
1999                |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2000            let segment_id = outcome
2001                .source_range_to_object
2002                .get(&arc_node_ref.range)
2003                .copied()
2004                .ok_or_else(|| make_err(format!("Source range of arc not found: {arc_node_ref:?}")))?;
2005            let segment_object = outcome
2006                .scene_objects
2007                .get(segment_id.0)
2008                .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2009            let ObjectKind::Segment { segment } = &segment_object.kind else {
2010                return Err(make_err(format!(
2011                    "Object is not a segment, it is {}",
2012                    segment_object.kind.human_friendly_kind_with_article()
2013                )));
2014            };
2015            let Segment::Arc(arc) = segment else {
2016                return Err(make_err(format!(
2017                    "Segment is not an arc, it is {}",
2018                    segment.human_friendly_kind_with_article()
2019                )));
2020            };
2021            vec![arc.start, arc.end, arc.center, segment_id]
2022        };
2023        let src_delta = SourceDelta { text: new_source };
2024        // Uses .no_freedom_analysis() so freedom_analysis: false
2025        let outcome = self.update_state_after_exec(outcome, false);
2026        let scene_graph_delta = SceneGraphDelta {
2027            new_graph: self.scene_graph.clone(),
2028            invalidates_ids: false,
2029            new_objects: new_object_ids,
2030            exec_outcome: outcome,
2031        };
2032        Ok((src_delta, scene_graph_delta))
2033    }
2034
2035    async fn add_circle(
2036        &mut self,
2037        ctx: &ExecutorContext,
2038        sketch: ObjectId,
2039        ctor: CircleCtor,
2040    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2041        // Create updated KCL source from args.
2042        let start_ast = to_ast_point2d(&ctor.start)
2043            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2044        let center_ast = to_ast_point2d(&ctor.center)
2045            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2046        let mut arguments = vec![
2047            ast::LabeledArg {
2048                label: Some(ast::Identifier::new(CIRCLE_START_PARAM)),
2049                arg: start_ast,
2050            },
2051            ast::LabeledArg {
2052                label: Some(ast::Identifier::new(CIRCLE_CENTER_PARAM)),
2053                arg: center_ast,
2054            },
2055        ];
2056        // Add construction kwarg if construction is Some(true)
2057        if ctor.construction == Some(true) {
2058            arguments.push(ast::LabeledArg {
2059                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2060                arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
2061                    value: ast::LiteralValue::Bool(true),
2062                    raw: "true".to_string(),
2063                    digest: None,
2064                }))),
2065            });
2066        }
2067        let circle_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
2068            callee: ast::Node::no_src(ast_sketch2_name(CIRCLE_FN)),
2069            unlabeled: None,
2070            arguments,
2071            digest: None,
2072            non_code_meta: Default::default(),
2073        })));
2074
2075        // Look up existing sketch.
2076        let sketch_id = sketch;
2077        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2078            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2079        })?;
2080        let ObjectKind::Sketch(_) = &sketch_object.kind else {
2081            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2082                "Object is not a sketch, it is {}",
2083                sketch_object.kind.human_friendly_kind_with_article(),
2084            ))));
2085        };
2086        // Add the circle to the AST of the sketch block.
2087        let mut new_ast = self.program.ast.clone();
2088        let (sketch_block_ref, _) = self
2089            .mutate_ast(
2090                &mut new_ast,
2091                sketch_id,
2092                AstMutateCommand::AddSketchBlockVarDecl {
2093                    prefix: CIRCLE_VARIABLE.to_owned(),
2094                    expr: circle_ast,
2095                },
2096            )
2097            .map_err(KclErrorWithOutputs::no_outputs)?;
2098        // Convert to string source to create real source ranges.
2099        let new_source = source_from_ast(&new_ast);
2100        // Parse the new KCL source.
2101        let (new_program, errors) = Program::parse(&new_source)
2102            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2103        if !errors.is_empty() {
2104            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2105                "Error parsing KCL source after adding circle: {errors:?}"
2106            ))));
2107        }
2108        let Some(new_program) = new_program else {
2109            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
2110                "No AST produced after adding circle".to_string(),
2111            )));
2112        };
2113
2114        let circle_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2115            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2116                "Source range of circle not found in sketch block: {sketch_block_ref:?}; {err:?}"
2117            )))
2118        })?;
2119
2120        // Make sure to only set this if there are no errors.
2121        self.program = new_program.clone();
2122
2123        // Truncate after the sketch block for mock execution.
2124        let mut truncated_program = new_program;
2125        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2126            .map_err(KclErrorWithOutputs::no_outputs)?;
2127
2128        // Execute.
2129        let outcome = ctx
2130            .run_mock(
2131                &truncated_program,
2132                &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2133            )
2134            .await?;
2135
2136        let new_object_ids = {
2137            let make_err =
2138                |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2139            let segment_id = outcome
2140                .source_range_to_object
2141                .get(&circle_node_ref.range)
2142                .copied()
2143                .ok_or_else(|| make_err(format!("Source range of circle not found: {circle_node_ref:?}")))?;
2144            let segment_object = outcome
2145                .scene_objects
2146                .get(segment_id.0)
2147                .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2148            let ObjectKind::Segment { segment } = &segment_object.kind else {
2149                return Err(make_err(format!(
2150                    "Object is not a segment, it is {}",
2151                    segment_object.kind.human_friendly_kind_with_article()
2152                )));
2153            };
2154            let Segment::Circle(circle) = segment else {
2155                return Err(make_err(format!(
2156                    "Segment is not a circle, it is {}",
2157                    segment.human_friendly_kind_with_article()
2158                )));
2159            };
2160            vec![circle.start, circle.center, segment_id]
2161        };
2162        let src_delta = SourceDelta { text: new_source };
2163        // Uses .no_freedom_analysis() so freedom_analysis: false
2164        let outcome = self.update_state_after_exec(outcome, false);
2165        let scene_graph_delta = SceneGraphDelta {
2166            new_graph: self.scene_graph.clone(),
2167            invalidates_ids: false,
2168            new_objects: new_object_ids,
2169            exec_outcome: outcome,
2170        };
2171        Ok((src_delta, scene_graph_delta))
2172    }
2173
2174    fn edit_point(
2175        &mut self,
2176        new_ast: &mut ast::Node<ast::Program>,
2177        sketch: ObjectId,
2178        point: ObjectId,
2179        ctor: PointCtor,
2180    ) -> Result<(), KclError> {
2181        // Create updated KCL source from args.
2182        let new_at_ast = to_ast_point2d(&ctor.position).map_err(|err| KclError::refactor(err.to_string()))?;
2183
2184        // Look up existing sketch.
2185        let sketch_id = sketch;
2186        let sketch_object = self
2187            .scene_graph
2188            .objects
2189            .get(sketch_id.0)
2190            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2191        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2192            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2193        };
2194        sketch.segments.iter().find(|o| **o == point).ok_or_else(|| {
2195            KclError::refactor(format!("Point not found in sketch: point={point:?}, sketch={sketch:?}"))
2196        })?;
2197        // Look up existing point.
2198        let point_id = point;
2199        let point_object = self
2200            .scene_graph
2201            .objects
2202            .get(point_id.0)
2203            .ok_or_else(|| KclError::refactor(format!("Point not found in scene graph: point={point:?}")))?;
2204        let ObjectKind::Segment {
2205            segment: Segment::Point(point),
2206        } = &point_object.kind
2207        else {
2208            return Err(KclError::refactor(format!(
2209                "Object is not a point segment: {point_object:?}"
2210            )));
2211        };
2212
2213        // If the point is part of a line or arc, edit the line/arc instead.
2214        if let Some(owner_id) = point.owner {
2215            let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
2216                KclError::refactor(format!(
2217                    "Internal: Owner of point not found in scene graph: owner={owner_id:?}",
2218                ))
2219            })?;
2220            let ObjectKind::Segment { segment } = &owner_object.kind else {
2221                return Err(KclError::refactor(format!(
2222                    "Internal: Owner of point is not a segment, but found {}",
2223                    owner_object.kind.human_friendly_kind_with_article()
2224                )));
2225            };
2226
2227            // Handle Line owner
2228            if let Segment::Line(line) = segment {
2229                let SegmentCtor::Line(line_ctor) = &line.ctor else {
2230                    return Err(KclError::refactor(format!(
2231                        "Internal: Owner of point does not have line ctor, but found {}",
2232                        line.ctor.human_friendly_kind_with_article()
2233                    )));
2234                };
2235                let mut line_ctor = line_ctor.clone();
2236                // Which end of the line is this point?
2237                if line.start == point_id {
2238                    line_ctor.start = ctor.position;
2239                } else if line.end == point_id {
2240                    line_ctor.end = ctor.position;
2241                } else {
2242                    return Err(KclError::refactor(format!(
2243                        "Internal: Point is not part of owner's line segment: point={point_id:?}, line={owner_id:?}"
2244                    )));
2245                }
2246                return self.edit_line(new_ast, sketch_id, owner_id, line_ctor);
2247            }
2248
2249            // Handle Arc owner
2250            if let Segment::Arc(arc) = segment {
2251                let SegmentCtor::Arc(arc_ctor) = &arc.ctor else {
2252                    return Err(KclError::refactor(format!(
2253                        "Internal: Owner of point does not have arc ctor, but found {}",
2254                        arc.ctor.human_friendly_kind_with_article()
2255                    )));
2256                };
2257                let mut arc_ctor = arc_ctor.clone();
2258                // Which point of the arc is this? (center, start, or end)
2259                if arc.center == point_id {
2260                    arc_ctor.center = ctor.position;
2261                } else if arc.start == point_id {
2262                    arc_ctor.start = ctor.position;
2263                } else if arc.end == point_id {
2264                    arc_ctor.end = ctor.position;
2265                } else {
2266                    return Err(KclError::refactor(format!(
2267                        "Internal: Point is not part of owner's arc segment: point={point_id:?}, arc={owner_id:?}"
2268                    )));
2269                }
2270                return self.edit_arc(new_ast, sketch_id, owner_id, arc_ctor);
2271            }
2272
2273            // Handle Circle owner
2274            if let Segment::Circle(circle) = segment {
2275                let SegmentCtor::Circle(circle_ctor) = &circle.ctor else {
2276                    return Err(KclError::refactor(format!(
2277                        "Internal: Owner of point does not have circle ctor, but found {}",
2278                        circle.ctor.human_friendly_kind_with_article()
2279                    )));
2280                };
2281                let mut circle_ctor = circle_ctor.clone();
2282                if circle.center == point_id {
2283                    circle_ctor.center = ctor.position;
2284                } else if circle.start == point_id {
2285                    circle_ctor.start = ctor.position;
2286                } else {
2287                    return Err(KclError::refactor(format!(
2288                        "Internal: Point is not part of owner's circle segment: point={point_id:?}, circle={owner_id:?}"
2289                    )));
2290                }
2291                return self.edit_circle(new_ast, sketch_id, owner_id, circle_ctor);
2292            }
2293
2294            // If owner is neither Line, Arc, nor Circle, allow editing the point directly
2295            // (fall through to the point editing logic below)
2296        }
2297
2298        // Modify the point AST.
2299        self.mutate_ast(new_ast, point_id, AstMutateCommand::EditPoint { at: new_at_ast })?;
2300        Ok(())
2301    }
2302
2303    fn edit_line(
2304        &mut self,
2305        new_ast: &mut ast::Node<ast::Program>,
2306        sketch: ObjectId,
2307        line: ObjectId,
2308        ctor: LineCtor,
2309    ) -> Result<(), KclError> {
2310        // Create updated KCL source from args.
2311        let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2312        let new_end_ast = to_ast_point2d(&ctor.end).map_err(|err| KclError::refactor(err.to_string()))?;
2313
2314        // Look up existing sketch.
2315        let sketch_id = sketch;
2316        let sketch_object = self
2317            .scene_graph
2318            .objects
2319            .get(sketch_id.0)
2320            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2321        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2322            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2323        };
2324        sketch
2325            .segments
2326            .iter()
2327            .find(|o| **o == line)
2328            .ok_or_else(|| KclError::refactor(format!("Line not found in sketch: line={line:?}, sketch={sketch:?}")))?;
2329        // Look up existing line.
2330        let line_id = line;
2331        let line_object = self
2332            .scene_graph
2333            .objects
2334            .get(line_id.0)
2335            .ok_or_else(|| KclError::refactor(format!("Line not found in scene graph: line={line:?}")))?;
2336        let ObjectKind::Segment { .. } = &line_object.kind else {
2337            let kind = line_object.kind.human_friendly_kind_with_article();
2338            return Err(KclError::refactor(format!(
2339                "This constraint only works on Segments, but you selected {kind}"
2340            )));
2341        };
2342
2343        // Modify the line AST.
2344        self.mutate_ast(
2345            new_ast,
2346            line_id,
2347            AstMutateCommand::EditLine {
2348                start: new_start_ast,
2349                end: new_end_ast,
2350                construction: ctor.construction,
2351            },
2352        )?;
2353        Ok(())
2354    }
2355
2356    fn edit_arc(
2357        &mut self,
2358        new_ast: &mut ast::Node<ast::Program>,
2359        sketch: ObjectId,
2360        arc: ObjectId,
2361        ctor: ArcCtor,
2362    ) -> Result<(), KclError> {
2363        // Create updated KCL source from args.
2364        let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2365        let new_end_ast = to_ast_point2d(&ctor.end).map_err(|err| KclError::refactor(err.to_string()))?;
2366        let new_center_ast = to_ast_point2d(&ctor.center).map_err(|err| KclError::refactor(err.to_string()))?;
2367
2368        // Look up existing sketch.
2369        let sketch_id = sketch;
2370        let sketch_object = self
2371            .scene_graph
2372            .objects
2373            .get(sketch_id.0)
2374            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2375        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2376            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2377        };
2378        sketch
2379            .segments
2380            .iter()
2381            .find(|o| **o == arc)
2382            .ok_or_else(|| KclError::refactor(format!("Arc not found in sketch: arc={arc:?}, sketch={sketch:?}")))?;
2383        // Look up existing arc.
2384        let arc_id = arc;
2385        let arc_object = self
2386            .scene_graph
2387            .objects
2388            .get(arc_id.0)
2389            .ok_or_else(|| KclError::refactor(format!("Arc not found in scene graph: arc={arc:?}")))?;
2390        let ObjectKind::Segment { .. } = &arc_object.kind else {
2391            return Err(KclError::refactor(format!("Object is not a segment: {arc_object:?}")));
2392        };
2393
2394        // Modify the arc AST.
2395        self.mutate_ast(
2396            new_ast,
2397            arc_id,
2398            AstMutateCommand::EditArc {
2399                start: new_start_ast,
2400                end: new_end_ast,
2401                center: new_center_ast,
2402                construction: ctor.construction,
2403            },
2404        )?;
2405        Ok(())
2406    }
2407
2408    fn edit_circle(
2409        &mut self,
2410        new_ast: &mut ast::Node<ast::Program>,
2411        sketch: ObjectId,
2412        circle: ObjectId,
2413        ctor: CircleCtor,
2414    ) -> Result<(), KclError> {
2415        // Create updated KCL source from args.
2416        let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2417        let new_center_ast = to_ast_point2d(&ctor.center).map_err(|err| KclError::refactor(err.to_string()))?;
2418
2419        // Look up existing sketch.
2420        let sketch_id = sketch;
2421        let sketch_object = self
2422            .scene_graph
2423            .objects
2424            .get(sketch_id.0)
2425            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2426        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2427            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2428        };
2429        sketch.segments.iter().find(|o| **o == circle).ok_or_else(|| {
2430            KclError::refactor(format!(
2431                "Circle not found in sketch: circle={circle:?}, sketch={sketch:?}"
2432            ))
2433        })?;
2434        // Look up existing circle.
2435        let circle_id = circle;
2436        let circle_object = self
2437            .scene_graph
2438            .objects
2439            .get(circle_id.0)
2440            .ok_or_else(|| KclError::refactor(format!("Circle not found in scene graph: circle={circle:?}")))?;
2441        let ObjectKind::Segment { .. } = &circle_object.kind else {
2442            return Err(KclError::refactor(format!(
2443                "Object is not a segment: {circle_object:?}"
2444            )));
2445        };
2446
2447        // Modify the circle AST.
2448        self.mutate_ast(
2449            new_ast,
2450            circle_id,
2451            AstMutateCommand::EditCircle {
2452                start: new_start_ast,
2453                center: new_center_ast,
2454                construction: ctor.construction,
2455            },
2456        )?;
2457        Ok(())
2458    }
2459
2460    fn delete_segment(
2461        &mut self,
2462        new_ast: &mut ast::Node<ast::Program>,
2463        sketch: ObjectId,
2464        segment_id: ObjectId,
2465    ) -> Result<(), KclError> {
2466        // Look up existing sketch.
2467        let sketch_id = sketch;
2468        let sketch_object = self
2469            .scene_graph
2470            .objects
2471            .get(sketch_id.0)
2472            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2473        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2474            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2475        };
2476        sketch.segments.iter().find(|o| **o == segment_id).ok_or_else(|| {
2477            KclError::refactor(format!(
2478                "Segment not found in sketch: segment={segment_id:?}, sketch={sketch:?}"
2479            ))
2480        })?;
2481        // Look up existing segment.
2482        let segment_object =
2483            self.scene_graph.objects.get(segment_id.0).ok_or_else(|| {
2484                KclError::refactor(format!("Segment not found in scene graph: segment={segment_id:?}"))
2485            })?;
2486        let ObjectKind::Segment { .. } = &segment_object.kind else {
2487            return Err(KclError::refactor(format!(
2488                "Object is not a segment, it is {}",
2489                segment_object.kind.human_friendly_kind_with_article()
2490            )));
2491        };
2492
2493        // Modify the AST to remove the segment.
2494        self.mutate_ast(new_ast, segment_id, AstMutateCommand::DeleteNode)?;
2495        Ok(())
2496    }
2497
2498    fn delete_constraint(
2499        &mut self,
2500        new_ast: &mut ast::Node<ast::Program>,
2501        sketch: ObjectId,
2502        constraint_id: ObjectId,
2503    ) -> Result<(), KclError> {
2504        // Look up existing sketch.
2505        let sketch_id = sketch;
2506        let sketch_object = self
2507            .scene_graph
2508            .objects
2509            .get(sketch_id.0)
2510            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2511        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2512            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2513        };
2514        sketch
2515            .constraints
2516            .iter()
2517            .find(|o| **o == constraint_id)
2518            .ok_or_else(|| {
2519                KclError::refactor(format!(
2520                    "Constraint not found in sketch: constraint={constraint_id:?}, sketch={sketch:?}"
2521                ))
2522            })?;
2523        // Look up existing constraint.
2524        let constraint_object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
2525            KclError::refactor(format!(
2526                "Constraint not found in scene graph: constraint={constraint_id:?}"
2527            ))
2528        })?;
2529        let ObjectKind::Constraint { .. } = &constraint_object.kind else {
2530            return Err(KclError::refactor(format!(
2531                "Object is not a constraint, it is {}",
2532                constraint_object.kind.human_friendly_kind_with_article()
2533            )));
2534        };
2535
2536        // Modify the AST to remove the constraint.
2537        self.mutate_ast(new_ast, constraint_id, AstMutateCommand::DeleteNode)?;
2538        Ok(())
2539    }
2540
2541    fn edit_coincident_constraint(
2542        &mut self,
2543        new_ast: &mut ast::Node<ast::Program>,
2544        constraint_id: ObjectId,
2545        segments: Vec<ConstraintSegment>,
2546    ) -> Result<(), KclError> {
2547        if segments.len() < 2 {
2548            return Err(KclError::refactor(format!(
2549                "Coincident constraint must have at least 2 inputs, got {}",
2550                segments.len()
2551            )));
2552        }
2553
2554        let segment_asts = segments
2555            .iter()
2556            .map(|segment| self.coincident_segment_to_ast(segment, new_ast))
2557            .collect::<Result<Vec<_>, _>>()?;
2558
2559        let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
2560            elements: segment_asts,
2561            digest: None,
2562            non_code_meta: Default::default(),
2563        })));
2564
2565        self.mutate_ast(
2566            new_ast,
2567            constraint_id,
2568            AstMutateCommand::EditCallUnlabeled { arg: array_expr },
2569        )?;
2570        Ok(())
2571    }
2572
2573    fn edit_horizontal_points_constraint(
2574        &mut self,
2575        new_ast: &mut ast::Node<ast::Program>,
2576        constraint_id: ObjectId,
2577        points: Vec<ConstraintSegment>,
2578    ) -> Result<(), KclError> {
2579        self.edit_axis_points_constraint(new_ast, constraint_id, points, "Horizontal")
2580    }
2581
2582    fn edit_vertical_points_constraint(
2583        &mut self,
2584        new_ast: &mut ast::Node<ast::Program>,
2585        constraint_id: ObjectId,
2586        points: Vec<ConstraintSegment>,
2587    ) -> Result<(), KclError> {
2588        self.edit_axis_points_constraint(new_ast, constraint_id, points, "Vertical")
2589    }
2590
2591    fn edit_axis_points_constraint(
2592        &mut self,
2593        new_ast: &mut ast::Node<ast::Program>,
2594        constraint_id: ObjectId,
2595        points: Vec<ConstraintSegment>,
2596        constraint_name: &str,
2597    ) -> Result<(), KclError> {
2598        if points.len() < 2 {
2599            return Err(KclError::refactor(format!(
2600                "{constraint_name} points constraint must have at least 2 points, got {}",
2601                points.len()
2602            )));
2603        }
2604
2605        let point_asts = points
2606            .iter()
2607            .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
2608            .collect::<Result<Vec<_>, _>>()?;
2609
2610        let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
2611            elements: point_asts,
2612            digest: None,
2613            non_code_meta: Default::default(),
2614        })));
2615
2616        self.mutate_ast(
2617            new_ast,
2618            constraint_id,
2619            AstMutateCommand::EditCallUnlabeled { arg: array_expr },
2620        )?;
2621        Ok(())
2622    }
2623
2624    /// updates the equalLength constraint with the given lines
2625    fn edit_equal_length_constraint(
2626        &mut self,
2627        new_ast: &mut ast::Node<ast::Program>,
2628        constraint_id: ObjectId,
2629        lines: Vec<ObjectId>,
2630    ) -> Result<(), KclError> {
2631        if lines.len() < 2 {
2632            return Err(KclError::refactor(format!(
2633                "Lines equal length constraint must have at least 2 lines, got {}",
2634                lines.len()
2635            )));
2636        }
2637
2638        let line_asts = lines
2639            .iter()
2640            .map(|line_id| {
2641                let line_object = self
2642                    .scene_graph
2643                    .objects
2644                    .get(line_id.0)
2645                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
2646                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
2647                    let kind = line_object.kind.human_friendly_kind_with_article();
2648                    return Err(KclError::refactor(format!(
2649                        "This constraint only works on Segments, but you selected {kind}"
2650                    )));
2651                };
2652                let Segment::Line(_) = line_segment else {
2653                    let kind = line_segment.human_friendly_kind_with_article();
2654                    return Err(KclError::refactor(format!(
2655                        "Only lines can be made equal length, but you selected {kind}"
2656                    )));
2657                };
2658
2659                get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)
2660            })
2661            .collect::<Result<Vec<_>, _>>()?;
2662
2663        let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
2664            elements: line_asts,
2665            digest: None,
2666            non_code_meta: Default::default(),
2667        })));
2668
2669        self.mutate_ast(
2670            new_ast,
2671            constraint_id,
2672            AstMutateCommand::EditCallUnlabeled { arg: array_expr },
2673        )?;
2674        Ok(())
2675    }
2676
2677    /// Updates the parallel constraint with the given lines.
2678    fn edit_parallel_constraint(
2679        &mut self,
2680        new_ast: &mut ast::Node<ast::Program>,
2681        constraint_id: ObjectId,
2682        lines: Vec<ObjectId>,
2683    ) -> Result<(), KclError> {
2684        if lines.len() < 2 {
2685            return Err(KclError::refactor(format!(
2686                "Parallel constraint must have at least 2 lines, got {}",
2687                lines.len()
2688            )));
2689        }
2690
2691        let line_asts = lines
2692            .iter()
2693            .map(|line_id| {
2694                let line_object = self
2695                    .scene_graph
2696                    .objects
2697                    .get(line_id.0)
2698                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
2699                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
2700                    let kind = line_object.kind.human_friendly_kind_with_article();
2701                    return Err(KclError::refactor(format!(
2702                        "This constraint only works on Segments, but you selected {kind}"
2703                    )));
2704                };
2705                let Segment::Line(_) = line_segment else {
2706                    let kind = line_segment.human_friendly_kind_with_article();
2707                    return Err(KclError::refactor(format!(
2708                        "Only lines can be made parallel, but you selected {kind}"
2709                    )));
2710                };
2711
2712                get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)
2713            })
2714            .collect::<Result<Vec<_>, _>>()?;
2715
2716        let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
2717            elements: line_asts,
2718            digest: None,
2719            non_code_meta: Default::default(),
2720        })));
2721
2722        self.mutate_ast(
2723            new_ast,
2724            constraint_id,
2725            AstMutateCommand::EditCallUnlabeled { arg: array_expr },
2726        )?;
2727        Ok(())
2728    }
2729
2730    /// Updates the equalRadius constraint with the given segments.
2731    fn edit_equal_radius_constraint(
2732        &mut self,
2733        new_ast: &mut ast::Node<ast::Program>,
2734        constraint_id: ObjectId,
2735        input: Vec<ObjectId>,
2736    ) -> Result<(), KclError> {
2737        if input.len() < 2 {
2738            return Err(KclError::refactor(format!(
2739                "equalRadius constraint must have at least 2 segments, got {}",
2740                input.len()
2741            )));
2742        }
2743
2744        let input_asts = input
2745            .iter()
2746            .map(|segment_id| self.equal_radius_segment_id_to_ast_reference(*segment_id, new_ast))
2747            .collect::<Result<Vec<_>, _>>()?;
2748
2749        let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
2750            elements: input_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    async fn execute_after_edit(
2764        &mut self,
2765        ctx: &ExecutorContext,
2766        sketch: ObjectId,
2767        sketch_block_ref: AstNodeRef,
2768        segment_ids_edited: AhashIndexSet<ObjectId>,
2769        edit_kind: EditDeleteKind,
2770        new_ast: &mut ast::Node<ast::Program>,
2771    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2772        // Convert to string source to create real source ranges.
2773        let new_source = source_from_ast(new_ast);
2774        // Parse the new KCL source.
2775        let (new_program, errors) = Program::parse(&new_source)
2776            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2777        if !errors.is_empty() {
2778            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2779                "Error parsing KCL source after editing: {errors:?}"
2780            ))));
2781        }
2782        let Some(new_program) = new_program else {
2783            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
2784                "No AST produced after editing".to_string(),
2785            )));
2786        };
2787
2788        // TODO: sketch-api: make sure to only set this if there are no errors.
2789        self.program = new_program.clone();
2790
2791        // Truncate after the sketch block for mock execution.
2792        let is_delete = edit_kind.is_delete();
2793        let truncated_program = {
2794            let mut truncated_program = new_program;
2795            only_sketch_block(
2796                &mut truncated_program.ast,
2797                &sketch_block_ref,
2798                edit_kind.to_change_kind(),
2799            )
2800            .map_err(KclErrorWithOutputs::no_outputs)?;
2801            truncated_program
2802        };
2803
2804        // Execute.
2805        let mock_config = MockConfig {
2806            sketch_block_id: Some(sketch),
2807            freedom_analysis: is_delete,
2808            segment_ids_edited: segment_ids_edited.clone(),
2809            ..Default::default()
2810        };
2811        let outcome = ctx.run_mock(&truncated_program, &mock_config).await?;
2812
2813        // Uses freedom_analysis: is_delete
2814        let outcome = self.update_state_after_exec(outcome, is_delete);
2815
2816        let new_source = {
2817            // Feed back sketch var solutions into the source.
2818            //
2819            // The interpreter is returning all var solutions from the sketch
2820            // block we're editing.
2821            let mut new_ast = self.program.ast.clone();
2822            for (var_range, value) in &outcome.var_solutions {
2823                let rounded = value.round(3);
2824                let source_ref = SourceRef::Simple {
2825                    range: *var_range,
2826                    node_path: None,
2827                };
2828                mutate_ast_node_by_source_ref(
2829                    &mut new_ast,
2830                    &source_ref,
2831                    AstMutateCommand::EditVarInitialValue { value: rounded },
2832                )
2833                .map_err(|err| KclErrorWithOutputs::from_error_outcome(err, outcome.clone()))?;
2834            }
2835            source_from_ast(&new_ast)
2836        };
2837
2838        let src_delta = SourceDelta { text: new_source };
2839        let scene_graph_delta = SceneGraphDelta {
2840            new_graph: self.scene_graph.clone(),
2841            invalidates_ids: is_delete,
2842            new_objects: Vec::new(),
2843            exec_outcome: outcome,
2844        };
2845        Ok((src_delta, scene_graph_delta))
2846    }
2847
2848    async fn execute_after_delete_sketch(
2849        &mut self,
2850        ctx: &ExecutorContext,
2851        new_ast: &mut ast::Node<ast::Program>,
2852    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2853        // Convert to string source to create real source ranges.
2854        let new_source = source_from_ast(new_ast);
2855        // Parse the new KCL source.
2856        let (new_program, errors) = Program::parse(&new_source)
2857            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2858        if !errors.is_empty() {
2859            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2860                "Error parsing KCL source after editing: {errors:?}"
2861            ))));
2862        }
2863        let Some(new_program) = new_program else {
2864            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
2865                "No AST produced after editing".to_string(),
2866            )));
2867        };
2868
2869        // Make sure to only set this if there are no errors.
2870        self.program = new_program.clone();
2871
2872        // We deleted the entire sketch block. It doesn't make sense to truncate
2873        // and execute only the sketch block. We execute the whole program with
2874        // a real engine.
2875
2876        // Execute.
2877        let outcome = ctx.run_with_caching(new_program).await?;
2878        let freedom_analysis_ran = true;
2879
2880        let outcome = self.update_state_after_exec(outcome, freedom_analysis_ran);
2881
2882        let src_delta = SourceDelta { text: new_source };
2883        let scene_graph_delta = SceneGraphDelta {
2884            new_graph: self.scene_graph.clone(),
2885            invalidates_ids: true,
2886            new_objects: Vec::new(),
2887            exec_outcome: outcome,
2888        };
2889        Ok((src_delta, scene_graph_delta))
2890    }
2891
2892    /// Map a point object id into an AST reference expression for use in
2893    /// constraints. If the point is owned by a segment (line or arc), we
2894    /// reference the appropriate property on that segment (e.g. `line1.start`,
2895    /// `arc1.center`). Otherwise we reference the point directly.
2896    fn point_id_to_ast_reference(
2897        &self,
2898        point_id: ObjectId,
2899        new_ast: &mut ast::Node<ast::Program>,
2900    ) -> Result<ast::Expr, KclError> {
2901        let point_object = self
2902            .scene_graph
2903            .objects
2904            .get(point_id.0)
2905            .ok_or_else(|| KclError::refactor(format!("Point not found: {point_id:?}")))?;
2906        let ObjectKind::Segment { segment: point_segment } = &point_object.kind else {
2907            return Err(KclError::refactor(format!("Object is not a segment: {point_object:?}")));
2908        };
2909        let Segment::Point(point) = point_segment else {
2910            return Err(KclError::refactor(format!(
2911                "Only points are currently supported: {point_object:?}"
2912            )));
2913        };
2914
2915        if let Some(owner_id) = point.owner {
2916            let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
2917                KclError::refactor(format!(
2918                    "Owner of point not found in scene graph: point={point_id:?}, owner={owner_id:?}"
2919                ))
2920            })?;
2921            let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind else {
2922                return Err(KclError::refactor(format!(
2923                    "Owner of point is not a segment, but found {}",
2924                    owner_object.kind.human_friendly_kind_with_article()
2925                )));
2926            };
2927
2928            match owner_segment {
2929                Segment::Line(line) => {
2930                    let property = if line.start == point_id {
2931                        LINE_PROPERTY_START
2932                    } else if line.end == point_id {
2933                        LINE_PROPERTY_END
2934                    } else {
2935                        return Err(KclError::refactor(format!(
2936                            "Internal: Point is not part of owner's line segment: point={point_id:?}, line={owner_id:?}"
2937                        )));
2938                    };
2939                    get_or_insert_ast_reference(new_ast, &owner_object.source, LINE_VARIABLE, Some(property))
2940                }
2941                Segment::Arc(arc) => {
2942                    let property = if arc.start == point_id {
2943                        ARC_PROPERTY_START
2944                    } else if arc.end == point_id {
2945                        ARC_PROPERTY_END
2946                    } else if arc.center == point_id {
2947                        ARC_PROPERTY_CENTER
2948                    } else {
2949                        return Err(KclError::refactor(format!(
2950                            "Internal: Point is not part of owner's arc segment: point={point_id:?}, arc={owner_id:?}"
2951                        )));
2952                    };
2953                    get_or_insert_ast_reference(new_ast, &owner_object.source, ARC_VARIABLE, Some(property))
2954                }
2955                Segment::Circle(circle) => {
2956                    let property = if circle.start == point_id {
2957                        CIRCLE_PROPERTY_START
2958                    } else if circle.center == point_id {
2959                        CIRCLE_PROPERTY_CENTER
2960                    } else {
2961                        return Err(KclError::refactor(format!(
2962                            "Internal: Point is not part of owner's circle segment: point={point_id:?}, circle={owner_id:?}"
2963                        )));
2964                    };
2965                    get_or_insert_ast_reference(new_ast, &owner_object.source, CIRCLE_VARIABLE, Some(property))
2966                }
2967                _ => Err(KclError::refactor(format!(
2968                    "Internal: Owner of point is not a supported segment type for constraints: {owner_segment:?}"
2969                ))),
2970            }
2971        } else {
2972            // Standalone point.
2973            get_or_insert_ast_reference(new_ast, &point_object.source, "point", None)
2974        }
2975    }
2976
2977    fn coincident_segment_to_ast(
2978        &self,
2979        segment: &ConstraintSegment,
2980        new_ast: &mut ast::Node<ast::Program>,
2981    ) -> Result<ast::Expr, KclError> {
2982        match segment {
2983            ConstraintSegment::Origin(_) => Ok(ast_name_expr("ORIGIN".to_owned())),
2984            ConstraintSegment::Segment(segment_id) => {
2985                let segment_object = self
2986                    .scene_graph
2987                    .objects
2988                    .get(segment_id.0)
2989                    .ok_or_else(|| KclError::refactor(format!("Object not found: {segment_id:?}")))?;
2990                let ObjectKind::Segment { segment } = &segment_object.kind else {
2991                    return Err(KclError::refactor(format!(
2992                        "Object is not a segment, it is {}",
2993                        segment_object.kind.human_friendly_kind_with_article()
2994                    )));
2995                };
2996
2997                match segment {
2998                    Segment::Point(_) => self.point_id_to_ast_reference(*segment_id, new_ast),
2999                    Segment::Line(_) => {
3000                        get_or_insert_ast_reference(new_ast, &segment_object.source, LINE_VARIABLE, None)
3001                    }
3002                    Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, ARC_VARIABLE, None),
3003                    Segment::Circle(_) => {
3004                        get_or_insert_ast_reference(new_ast, &segment_object.source, CIRCLE_VARIABLE, None)
3005                    }
3006                }
3007            }
3008        }
3009    }
3010
3011    fn axis_constraint_segment_to_ast(
3012        &self,
3013        segment: &ConstraintSegment,
3014        new_ast: &mut ast::Node<ast::Program>,
3015    ) -> Result<ast::Expr, KclError> {
3016        match segment {
3017            ConstraintSegment::Origin(_) => Ok(ast_name_expr("ORIGIN".to_owned())),
3018            ConstraintSegment::Segment(point_id) => self.point_id_to_ast_reference(*point_id, new_ast),
3019        }
3020    }
3021
3022    async fn add_coincident(
3023        &mut self,
3024        sketch: ObjectId,
3025        coincident: Coincident,
3026        new_ast: &mut ast::Node<ast::Program>,
3027    ) -> Result<AstNodeRef, KclError> {
3028        let sketch_id = sketch;
3029        let segment_asts = coincident
3030            .segments
3031            .iter()
3032            .map(|segment| self.coincident_segment_to_ast(segment, new_ast))
3033            .collect::<Result<Vec<_>, _>>()?;
3034        if segment_asts.len() < 2 {
3035            return Err(KclError::refactor(format!(
3036                "Coincident constraint must have at least 2 inputs, got {}",
3037                segment_asts.len()
3038            )));
3039        }
3040
3041        // Create the coincident() call using shared helper.
3042        let coincident_ast = create_coincident_ast(segment_asts);
3043
3044        // Add the line to the AST of the sketch block.
3045        let (sketch_block_ref, _) = self.mutate_ast(
3046            new_ast,
3047            sketch_id,
3048            AstMutateCommand::AddSketchBlockExprStmt { expr: coincident_ast },
3049        )?;
3050        Ok(sketch_block_ref)
3051    }
3052
3053    async fn add_distance(
3054        &mut self,
3055        sketch: ObjectId,
3056        distance: Distance,
3057        new_ast: &mut ast::Node<ast::Program>,
3058    ) -> Result<AstNodeRef, KclError> {
3059        let sketch_id = sketch;
3060        let [pt0_ast, pt1_ast] = match distance.points.as_slice() {
3061            [pt0, pt1] => [
3062                self.coincident_segment_to_ast(pt0, new_ast)?,
3063                self.coincident_segment_to_ast(pt1, new_ast)?,
3064            ],
3065            _ => {
3066                return Err(KclError::refactor(format!(
3067                    "Distance constraint must have exactly 2 points, got {}",
3068                    distance.points.len()
3069                )));
3070            }
3071        };
3072
3073        let arguments = match &distance.label_position {
3074            Some(label_position) => vec![ast::LabeledArg {
3075                label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
3076                arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
3077            }],
3078            None => Default::default(),
3079        };
3080
3081        // Create the distance() call.
3082        let distance_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3083            callee: ast::Node::no_src(ast_sketch2_name(DISTANCE_FN)),
3084            unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
3085                ast::ArrayExpression {
3086                    elements: vec![pt0_ast, pt1_ast],
3087                    digest: None,
3088                    non_code_meta: Default::default(),
3089                },
3090            )))),
3091            arguments,
3092            digest: None,
3093            non_code_meta: Default::default(),
3094        })));
3095        let distance_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
3096            left: distance_call_ast,
3097            operator: ast::BinaryOperator::Eq,
3098            right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
3099                value: ast::LiteralValue::Number {
3100                    value: distance.distance.value,
3101                    suffix: distance.distance.units,
3102                },
3103                raw: format_number_literal(distance.distance.value, distance.distance.units, None).map_err(|_| {
3104                    KclError::refactor(format!(
3105                        "Could not format numeric suffix: {:?}",
3106                        distance.distance.units
3107                    ))
3108                })?,
3109                digest: None,
3110            }))),
3111            digest: None,
3112        })));
3113
3114        // Add the line to the AST of the sketch block.
3115        let (sketch_block_ref, _) = self.mutate_ast(
3116            new_ast,
3117            sketch_id,
3118            AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
3119        )?;
3120        Ok(sketch_block_ref)
3121    }
3122
3123    async fn add_angle(
3124        &mut self,
3125        sketch: ObjectId,
3126        angle: Angle,
3127        new_ast: &mut ast::Node<ast::Program>,
3128    ) -> Result<AstNodeRef, KclError> {
3129        let &[l0_id, l1_id] = angle.lines.as_slice() else {
3130            return Err(KclError::refactor(format!(
3131                "Angle constraint must have exactly 2 lines, got {}",
3132                angle.lines.len()
3133            )));
3134        };
3135        let sketch_id = sketch;
3136
3137        // Map the runtime objects back to variable names.
3138        let line0_object = self
3139            .scene_graph
3140            .objects
3141            .get(l0_id.0)
3142            .ok_or_else(|| KclError::refactor(format!("Line not found: {l0_id:?}")))?;
3143        let ObjectKind::Segment { segment: line0_segment } = &line0_object.kind else {
3144            return Err(KclError::refactor(format!("Object is not a segment: {line0_object:?}")));
3145        };
3146        let Segment::Line(_) = line0_segment else {
3147            return Err(KclError::refactor(format!(
3148                "Only lines can be constrained to meet at an angle: {line0_object:?}",
3149            )));
3150        };
3151        let l0_ast = get_or_insert_ast_reference(new_ast, &line0_object.source.clone(), LINE_VARIABLE, None)?;
3152
3153        let line1_object = self
3154            .scene_graph
3155            .objects
3156            .get(l1_id.0)
3157            .ok_or_else(|| KclError::refactor(format!("Line not found: {l1_id:?}")))?;
3158        let ObjectKind::Segment { segment: line1_segment } = &line1_object.kind else {
3159            return Err(KclError::refactor(format!("Object is not a segment: {line1_object:?}")));
3160        };
3161        let Segment::Line(_) = line1_segment else {
3162            return Err(KclError::refactor(format!(
3163                "Only lines can be constrained to meet at an angle: {line1_object:?}",
3164            )));
3165        };
3166        let l1_ast = get_or_insert_ast_reference(new_ast, &line1_object.source.clone(), LINE_VARIABLE, None)?;
3167
3168        // Create the angle() call.
3169        let angle_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3170            callee: ast::Node::no_src(ast_sketch2_name(ANGLE_FN)),
3171            unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
3172                ast::ArrayExpression {
3173                    elements: vec![l0_ast, l1_ast],
3174                    digest: None,
3175                    non_code_meta: Default::default(),
3176                },
3177            )))),
3178            arguments: Default::default(),
3179            digest: None,
3180            non_code_meta: Default::default(),
3181        })));
3182        let angle_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
3183            left: angle_call_ast,
3184            operator: ast::BinaryOperator::Eq,
3185            right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
3186                value: ast::LiteralValue::Number {
3187                    value: angle.angle.value,
3188                    suffix: angle.angle.units,
3189                },
3190                raw: format_number_literal(angle.angle.value, angle.angle.units, None).map_err(|_| {
3191                    KclError::refactor(format!("Could not format numeric suffix: {:?}", angle.angle.units))
3192                })?,
3193                digest: None,
3194            }))),
3195            digest: None,
3196        })));
3197
3198        // Add the line to the AST of the sketch block.
3199        let (sketch_block_ref, _) = self.mutate_ast(
3200            new_ast,
3201            sketch_id,
3202            AstMutateCommand::AddSketchBlockExprStmt { expr: angle_ast },
3203        )?;
3204        Ok(sketch_block_ref)
3205    }
3206
3207    async fn add_tangent(
3208        &mut self,
3209        sketch: ObjectId,
3210        tangent: Tangent,
3211        new_ast: &mut ast::Node<ast::Program>,
3212    ) -> Result<AstNodeRef, KclError> {
3213        let &[seg0_id, seg1_id] = tangent.input.as_slice() else {
3214            return Err(KclError::refactor(format!(
3215                "Tangent constraint must have exactly 2 segments, got {}",
3216                tangent.input.len()
3217            )));
3218        };
3219        let sketch_id = sketch;
3220
3221        let seg0_object = self
3222            .scene_graph
3223            .objects
3224            .get(seg0_id.0)
3225            .ok_or_else(|| KclError::refactor(format!("Segment not found: {seg0_id:?}")))?;
3226        let ObjectKind::Segment { segment: seg0_segment } = &seg0_object.kind else {
3227            return Err(KclError::refactor(format!("Object is not a segment: {seg0_object:?}")));
3228        };
3229        let seg0_ast = match seg0_segment {
3230            Segment::Line(_) => get_or_insert_ast_reference(new_ast, &seg0_object.source, LINE_VARIABLE, None)?,
3231            Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &seg0_object.source, ARC_VARIABLE, None)?,
3232            Segment::Circle(_) => get_or_insert_ast_reference(new_ast, &seg0_object.source, CIRCLE_VARIABLE, None)?,
3233            _ => {
3234                return Err(KclError::refactor(format!(
3235                    "Tangent supports only line/arc/circle segments, got: {seg0_segment:?}"
3236                )));
3237            }
3238        };
3239
3240        let seg1_object = self
3241            .scene_graph
3242            .objects
3243            .get(seg1_id.0)
3244            .ok_or_else(|| KclError::refactor(format!("Segment not found: {seg1_id:?}")))?;
3245        let ObjectKind::Segment { segment: seg1_segment } = &seg1_object.kind else {
3246            return Err(KclError::refactor(format!("Object is not a segment: {seg1_object:?}")));
3247        };
3248        let seg1_ast = match seg1_segment {
3249            Segment::Line(_) => get_or_insert_ast_reference(new_ast, &seg1_object.source, LINE_VARIABLE, None)?,
3250            Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &seg1_object.source, ARC_VARIABLE, None)?,
3251            Segment::Circle(_) => get_or_insert_ast_reference(new_ast, &seg1_object.source, CIRCLE_VARIABLE, None)?,
3252            _ => {
3253                return Err(KclError::refactor(format!(
3254                    "Tangent supports only line/arc/circle segments, got: {seg1_segment:?}"
3255                )));
3256            }
3257        };
3258
3259        let tangent_ast = create_tangent_ast(seg0_ast, seg1_ast);
3260        let (sketch_block_ref, _) = self.mutate_ast(
3261            new_ast,
3262            sketch_id,
3263            AstMutateCommand::AddSketchBlockExprStmt { expr: tangent_ast },
3264        )?;
3265        Ok(sketch_block_ref)
3266    }
3267
3268    async fn add_symmetric(
3269        &mut self,
3270        sketch: ObjectId,
3271        symmetric: Symmetric,
3272        new_ast: &mut ast::Node<ast::Program>,
3273    ) -> Result<AstNodeRef, KclError> {
3274        let &[input0_id, input1_id] = symmetric.input.as_slice() else {
3275            return Err(KclError::refactor(format!(
3276                "Symmetric constraint must have exactly 2 inputs, got {}",
3277                symmetric.input.len()
3278            )));
3279        };
3280        let sketch_id = sketch;
3281
3282        let input0_ast = self.symmetric_input_id_to_ast_reference(input0_id, new_ast)?;
3283        let input1_ast = self.symmetric_input_id_to_ast_reference(input1_id, new_ast)?;
3284        let axis_ast = self.symmetric_axis_id_to_ast_reference(symmetric.axis, new_ast)?;
3285
3286        let symmetric_ast = create_symmetric_ast(vec![input0_ast, input1_ast], axis_ast);
3287        let (sketch_block_ref, _) = self.mutate_ast(
3288            new_ast,
3289            sketch_id,
3290            AstMutateCommand::AddSketchBlockExprStmt { expr: symmetric_ast },
3291        )?;
3292        Ok(sketch_block_ref)
3293    }
3294
3295    async fn add_midpoint(
3296        &mut self,
3297        sketch: ObjectId,
3298        midpoint: Midpoint,
3299        new_ast: &mut ast::Node<ast::Program>,
3300    ) -> Result<AstNodeRef, KclError> {
3301        let sketch_id = sketch;
3302        let point_ast = self.point_id_to_ast_reference(midpoint.point, new_ast)?;
3303
3304        let segment_object = self
3305            .scene_graph
3306            .objects
3307            .get(midpoint.segment.0)
3308            .ok_or_else(|| KclError::refactor(format!("Segment not found: {:?}", midpoint.segment)))?;
3309        let ObjectKind::Segment {
3310            segment: midpoint_segment,
3311        } = &segment_object.kind
3312        else {
3313            return Err(KclError::refactor(format!(
3314                "Object must be a segment, but it was {}",
3315                segment_object.kind.human_friendly_kind_with_article()
3316            )));
3317        };
3318        let segment_ast = match midpoint_segment {
3319            Segment::Line(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, "line", None)?,
3320            Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, "arc", None)?,
3321            _ => {
3322                return Err(KclError::refactor(format!(
3323                    "Midpoint target must be a line or arc segment but it was {}",
3324                    midpoint_segment.human_friendly_kind_with_article()
3325                )));
3326            }
3327        };
3328
3329        let midpoint_ast = create_midpoint_ast(segment_ast, point_ast);
3330        let (sketch_block_ref, _) = self.mutate_ast(
3331            new_ast,
3332            sketch_id,
3333            AstMutateCommand::AddSketchBlockExprStmt { expr: midpoint_ast },
3334        )?;
3335        Ok(sketch_block_ref)
3336    }
3337
3338    async fn add_equal_radius(
3339        &mut self,
3340        sketch: ObjectId,
3341        equal_radius: EqualRadius,
3342        new_ast: &mut ast::Node<ast::Program>,
3343    ) -> Result<AstNodeRef, KclError> {
3344        if equal_radius.input.len() < 2 {
3345            return Err(KclError::refactor(format!(
3346                "equalRadius constraint must have at least 2 segments, got {}",
3347                equal_radius.input.len()
3348            )));
3349        }
3350
3351        let sketch_id = sketch;
3352        let input_asts = equal_radius
3353            .input
3354            .iter()
3355            .map(|segment_id| self.equal_radius_segment_id_to_ast_reference(*segment_id, new_ast))
3356            .collect::<Result<Vec<_>, _>>()?;
3357
3358        let equal_radius_ast = create_equal_radius_ast(input_asts);
3359        let (sketch_block_ref, _) = self.mutate_ast(
3360            new_ast,
3361            sketch_id,
3362            AstMutateCommand::AddSketchBlockExprStmt { expr: equal_radius_ast },
3363        )?;
3364        Ok(sketch_block_ref)
3365    }
3366
3367    async fn add_radius(
3368        &mut self,
3369        sketch: ObjectId,
3370        radius: Radius,
3371        new_ast: &mut ast::Node<ast::Program>,
3372    ) -> Result<AstNodeRef, KclError> {
3373        let params = ArcSizeConstraintParams {
3374            points: vec![radius.arc],
3375            function_name: RADIUS_FN,
3376            value: radius.radius.value,
3377            units: radius.radius.units,
3378            label_position: radius.label_position,
3379            constraint_type_name: "Radius",
3380        };
3381        self.add_arc_size_constraint(sketch, params, new_ast).await
3382    }
3383
3384    async fn add_diameter(
3385        &mut self,
3386        sketch: ObjectId,
3387        diameter: Diameter,
3388        new_ast: &mut ast::Node<ast::Program>,
3389    ) -> Result<AstNodeRef, KclError> {
3390        let params = ArcSizeConstraintParams {
3391            points: vec![diameter.arc],
3392            function_name: DIAMETER_FN,
3393            value: diameter.diameter.value,
3394            units: diameter.diameter.units,
3395            label_position: diameter.label_position,
3396            constraint_type_name: "Diameter",
3397        };
3398        self.add_arc_size_constraint(sketch, params, new_ast).await
3399    }
3400
3401    async fn add_fixed_constraints(
3402        &mut self,
3403        sketch: ObjectId,
3404        points: Vec<FixedPoint>,
3405        new_ast: &mut ast::Node<ast::Program>,
3406    ) -> Result<AstNodeRef, KclError> {
3407        let mut sketch_block_ref = None;
3408
3409        for fixed_point in points {
3410            let point_ast = self.point_id_to_ast_reference(fixed_point.point, new_ast)?;
3411            let fixed_ast = create_fixed_point_constraint_ast(point_ast, fixed_point.position)
3412                .map_err(|err| KclError::refactor(err.to_string()))?;
3413
3414            let (sketch_ref, _) = self.mutate_ast(
3415                new_ast,
3416                sketch,
3417                AstMutateCommand::AddSketchBlockExprStmt { expr: fixed_ast },
3418            )?;
3419            sketch_block_ref = Some(sketch_ref);
3420        }
3421
3422        sketch_block_ref.ok_or_else(|| KclError::refactor("Fixed constraint requires at least one point".to_owned()))
3423    }
3424
3425    async fn add_arc_size_constraint(
3426        &mut self,
3427        sketch: ObjectId,
3428        params: ArcSizeConstraintParams,
3429        new_ast: &mut ast::Node<ast::Program>,
3430    ) -> Result<AstNodeRef, KclError> {
3431        let sketch_id = sketch;
3432
3433        // Constraint must have exactly 1 argument (arc segment)
3434        if params.points.len() != 1 {
3435            return Err(KclError::refactor(format!(
3436                "{} constraint must have exactly 1 argument (an arc segment), got {}",
3437                params.constraint_type_name,
3438                params.points.len()
3439            )));
3440        }
3441
3442        let arc_id = params.points[0];
3443        let arc_object = self
3444            .scene_graph
3445            .objects
3446            .get(arc_id.0)
3447            .ok_or_else(|| KclError::refactor(format!("Arc segment not found: {arc_id:?}")))?;
3448        let ObjectKind::Segment { segment: arc_segment } = &arc_object.kind else {
3449            return Err(KclError::refactor(format!("Object is not a segment: {arc_object:?}")));
3450        };
3451        let ref_type = match arc_segment {
3452            Segment::Arc(_) => ARC_VARIABLE,
3453            Segment::Circle(_) => CIRCLE_VARIABLE,
3454            _ => {
3455                return Err(KclError::refactor(format!(
3456                    "{} constraint argument must be an arc or circle segment, got: {arc_segment:?}",
3457                    params.constraint_type_name
3458                )));
3459            }
3460        };
3461        // Reference the arc/circle segment directly
3462        let arc_ast = get_or_insert_ast_reference(new_ast, &arc_object.source, ref_type, None)?;
3463        let arguments = match &params.label_position {
3464            Some(label_position) => vec![ast::LabeledArg {
3465                label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
3466                arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
3467            }],
3468            None => Default::default(),
3469        };
3470
3471        // Create the function call.
3472        let call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3473            callee: ast::Node::no_src(ast_sketch2_name(params.function_name)),
3474            unlabeled: Some(arc_ast),
3475            arguments,
3476            digest: None,
3477            non_code_meta: Default::default(),
3478        })));
3479        let constraint_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
3480            left: call_ast,
3481            operator: ast::BinaryOperator::Eq,
3482            right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
3483                value: ast::LiteralValue::Number {
3484                    value: params.value,
3485                    suffix: params.units,
3486                },
3487                raw: format_number_literal(params.value, params.units, None)
3488                    .map_err(|_| KclError::refactor(format!("Could not format numeric suffix: {:?}", params.units)))?,
3489                digest: None,
3490            }))),
3491            digest: None,
3492        })));
3493
3494        // Add the line to the AST of the sketch block.
3495        let (sketch_block_ref, _) = self.mutate_ast(
3496            new_ast,
3497            sketch_id,
3498            AstMutateCommand::AddSketchBlockExprStmt { expr: constraint_ast },
3499        )?;
3500        Ok(sketch_block_ref)
3501    }
3502
3503    async fn add_horizontal_distance(
3504        &mut self,
3505        sketch: ObjectId,
3506        distance: Distance,
3507        new_ast: &mut ast::Node<ast::Program>,
3508    ) -> Result<AstNodeRef, KclError> {
3509        let sketch_id = sketch;
3510        let [pt0_ast, pt1_ast] = match distance.points.as_slice() {
3511            [pt0, pt1] => [
3512                self.coincident_segment_to_ast(pt0, new_ast)?,
3513                self.coincident_segment_to_ast(pt1, new_ast)?,
3514            ],
3515            _ => {
3516                return Err(KclError::refactor(format!(
3517                    "Horizontal distance constraint must have exactly 2 points, got {}",
3518                    distance.points.len()
3519                )));
3520            }
3521        };
3522
3523        let arguments = match &distance.label_position {
3524            Some(label_position) => vec![ast::LabeledArg {
3525                label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
3526                arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
3527            }],
3528            None => Default::default(),
3529        };
3530
3531        // Create the horizontalDistance() call.
3532        let distance_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3533            callee: ast::Node::no_src(ast_sketch2_name(HORIZONTAL_DISTANCE_FN)),
3534            unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
3535                ast::ArrayExpression {
3536                    elements: vec![pt0_ast, pt1_ast],
3537                    digest: None,
3538                    non_code_meta: Default::default(),
3539                },
3540            )))),
3541            arguments,
3542            digest: None,
3543            non_code_meta: Default::default(),
3544        })));
3545        let distance_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
3546            left: distance_call_ast,
3547            operator: ast::BinaryOperator::Eq,
3548            right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
3549                value: ast::LiteralValue::Number {
3550                    value: distance.distance.value,
3551                    suffix: distance.distance.units,
3552                },
3553                raw: format_number_literal(distance.distance.value, distance.distance.units, None).map_err(|_| {
3554                    KclError::refactor(format!(
3555                        "Could not format numeric suffix: {:?}",
3556                        distance.distance.units
3557                    ))
3558                })?,
3559                digest: None,
3560            }))),
3561            digest: None,
3562        })));
3563
3564        // Add the line to the AST of the sketch block.
3565        let (sketch_block_ref, _) = self.mutate_ast(
3566            new_ast,
3567            sketch_id,
3568            AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
3569        )?;
3570        Ok(sketch_block_ref)
3571    }
3572
3573    async fn add_vertical_distance(
3574        &mut self,
3575        sketch: ObjectId,
3576        distance: Distance,
3577        new_ast: &mut ast::Node<ast::Program>,
3578    ) -> Result<AstNodeRef, KclError> {
3579        let sketch_id = sketch;
3580        let [pt0_ast, pt1_ast] = match distance.points.as_slice() {
3581            [pt0, pt1] => [
3582                self.coincident_segment_to_ast(pt0, new_ast)?,
3583                self.coincident_segment_to_ast(pt1, new_ast)?,
3584            ],
3585            _ => {
3586                return Err(KclError::refactor(format!(
3587                    "Vertical distance constraint must have exactly 2 points, got {}",
3588                    distance.points.len()
3589                )));
3590            }
3591        };
3592
3593        let arguments = match &distance.label_position {
3594            Some(label_position) => vec![ast::LabeledArg {
3595                label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
3596                arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
3597            }],
3598            None => Default::default(),
3599        };
3600
3601        // Create the verticalDistance() call.
3602        let distance_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3603            callee: ast::Node::no_src(ast_sketch2_name(VERTICAL_DISTANCE_FN)),
3604            unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
3605                ast::ArrayExpression {
3606                    elements: vec![pt0_ast, pt1_ast],
3607                    digest: None,
3608                    non_code_meta: Default::default(),
3609                },
3610            )))),
3611            arguments,
3612            digest: None,
3613            non_code_meta: Default::default(),
3614        })));
3615        let distance_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
3616            left: distance_call_ast,
3617            operator: ast::BinaryOperator::Eq,
3618            right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
3619                value: ast::LiteralValue::Number {
3620                    value: distance.distance.value,
3621                    suffix: distance.distance.units,
3622                },
3623                raw: format_number_literal(distance.distance.value, distance.distance.units, None).map_err(|_| {
3624                    KclError::refactor(format!(
3625                        "Could not format numeric suffix: {:?}",
3626                        distance.distance.units
3627                    ))
3628                })?,
3629                digest: None,
3630            }))),
3631            digest: None,
3632        })));
3633
3634        // Add the line to the AST of the sketch block.
3635        let (sketch_block_ref, _) = self.mutate_ast(
3636            new_ast,
3637            sketch_id,
3638            AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
3639        )?;
3640        Ok(sketch_block_ref)
3641    }
3642
3643    async fn add_horizontal(
3644        &mut self,
3645        sketch: ObjectId,
3646        horizontal: Horizontal,
3647        new_ast: &mut ast::Node<ast::Program>,
3648    ) -> Result<AstNodeRef, KclError> {
3649        let sketch_id = sketch;
3650
3651        // Map the runtime objects back to variable names.
3652        let first_arg_ast = match horizontal {
3653            Horizontal::Line { line } => {
3654                let line_object = self
3655                    .scene_graph
3656                    .objects
3657                    .get(line.0)
3658                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line:?}")))?;
3659                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3660                    let kind = line_object.kind.human_friendly_kind_with_article();
3661                    return Err(KclError::refactor(format!(
3662                        "This constraint only works on Segments, but you selected {kind}"
3663                    )));
3664                };
3665                let Segment::Line(_) = line_segment else {
3666                    return Err(KclError::refactor(format!(
3667                        "Only lines can be made horizontal, but you selected {}",
3668                        line_segment.human_friendly_kind_with_article(),
3669                    )));
3670                };
3671                get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)?
3672            }
3673            Horizontal::Points { points } => {
3674                let point_asts = points
3675                    .iter()
3676                    .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
3677                    .collect::<Result<Vec<_>, _>>()?;
3678                ast::ArrayExpression::new(point_asts).into()
3679            }
3680        };
3681
3682        // Create the horizontal() call using shared helper.
3683        let horizontal_ast = create_horizontal_ast(first_arg_ast);
3684
3685        // Add the line to the AST of the sketch block.
3686        let (sketch_block_ref, _) = self.mutate_ast(
3687            new_ast,
3688            sketch_id,
3689            AstMutateCommand::AddSketchBlockExprStmt { expr: horizontal_ast },
3690        )?;
3691        Ok(sketch_block_ref)
3692    }
3693
3694    async fn add_lines_equal_length(
3695        &mut self,
3696        sketch: ObjectId,
3697        lines_equal_length: LinesEqualLength,
3698        new_ast: &mut ast::Node<ast::Program>,
3699    ) -> Result<AstNodeRef, KclError> {
3700        if lines_equal_length.lines.len() < 2 {
3701            return Err(KclError::refactor(format!(
3702                "Lines equal length constraint must have at least 2 lines, got {}",
3703                lines_equal_length.lines.len()
3704            )));
3705        };
3706
3707        let sketch_id = sketch;
3708
3709        // Map the runtime objects back to variable names.
3710        let line_asts = lines_equal_length
3711            .lines
3712            .iter()
3713            .map(|line_id| {
3714                let line_object = self
3715                    .scene_graph
3716                    .objects
3717                    .get(line_id.0)
3718                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3719                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3720                    let kind = line_object.kind.human_friendly_kind_with_article();
3721                    return Err(KclError::refactor(format!(
3722                        "This constraint only works on Segments, but you selected {kind}"
3723                    )));
3724                };
3725                let Segment::Line(_) = line_segment else {
3726                    let kind = line_segment.human_friendly_kind_with_article();
3727                    return Err(KclError::refactor(format!(
3728                        "Only lines can be made equal length, but you selected {kind}"
3729                    )));
3730                };
3731
3732                get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)
3733            })
3734            .collect::<Result<Vec<_>, _>>()?;
3735
3736        // Create the equalLength() call using shared helper.
3737        let equal_length_ast = create_equal_length_ast(line_asts);
3738
3739        // Add the constraint to the AST of the sketch block.
3740        let (sketch_block_ref, _) = self.mutate_ast(
3741            new_ast,
3742            sketch_id,
3743            AstMutateCommand::AddSketchBlockExprStmt { expr: equal_length_ast },
3744        )?;
3745        Ok(sketch_block_ref)
3746    }
3747
3748    fn equal_radius_segment_id_to_ast_reference(
3749        &mut self,
3750        segment_id: ObjectId,
3751        new_ast: &mut ast::Node<ast::Program>,
3752    ) -> Result<ast::Expr, KclError> {
3753        let segment_object = self
3754            .scene_graph
3755            .objects
3756            .get(segment_id.0)
3757            .ok_or_else(|| KclError::refactor(format!("Segment not found: {segment_id:?}")))?;
3758        let ObjectKind::Segment { segment } = &segment_object.kind else {
3759            return Err(KclError::refactor(format!(
3760                "Object is not a segment, it was {}",
3761                segment_object.kind.human_friendly_kind_with_article()
3762            )));
3763        };
3764
3765        let ref_type = match segment {
3766            Segment::Arc(_) => ARC_VARIABLE,
3767            Segment::Circle(_) => CIRCLE_VARIABLE,
3768            _ => {
3769                return Err(KclError::refactor(format!(
3770                    "equalRadius supports only arc/circle segments, got {}",
3771                    segment.human_friendly_kind_with_article()
3772                )));
3773            }
3774        };
3775
3776        get_or_insert_ast_reference(new_ast, &segment_object.source, ref_type, None)
3777    }
3778
3779    fn symmetric_input_id_to_ast_reference(
3780        &mut self,
3781        segment_id: ObjectId,
3782        new_ast: &mut ast::Node<ast::Program>,
3783    ) -> Result<ast::Expr, KclError> {
3784        let segment_object = self
3785            .scene_graph
3786            .objects
3787            .get(segment_id.0)
3788            .ok_or_else(|| KclError::refactor(format!("Segment not found: {segment_id:?}")))?;
3789        let ObjectKind::Segment { segment } = &segment_object.kind else {
3790            return Err(KclError::refactor(format!(
3791                "Object is not a segment, it was {}",
3792                segment_object.kind.human_friendly_kind_with_article()
3793            )));
3794        };
3795
3796        match segment {
3797            Segment::Point(_) => self.point_id_to_ast_reference(segment_id, new_ast),
3798            Segment::Line(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, LINE_VARIABLE, None),
3799            Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, ARC_VARIABLE, None),
3800            Segment::Circle(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, CIRCLE_VARIABLE, None),
3801        }
3802    }
3803
3804    fn symmetric_axis_id_to_ast_reference(
3805        &mut self,
3806        segment_id: ObjectId,
3807        new_ast: &mut ast::Node<ast::Program>,
3808    ) -> Result<ast::Expr, KclError> {
3809        let segment_object = self
3810            .scene_graph
3811            .objects
3812            .get(segment_id.0)
3813            .ok_or_else(|| KclError::refactor(format!("Axis segment not found: {segment_id:?}")))?;
3814        let ObjectKind::Segment { segment } = &segment_object.kind else {
3815            return Err(KclError::refactor(format!(
3816                "Object is not a segment, it was {}",
3817                segment_object.kind.human_friendly_kind_with_article()
3818            )));
3819        };
3820        match segment {
3821            Segment::Line(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, LINE_VARIABLE, None),
3822            _ => Err(KclError::refactor(format!(
3823                "Symmetric axis must be a line, got {}",
3824                segment.human_friendly_kind_with_article()
3825            ))),
3826        }
3827    }
3828
3829    async fn add_parallel(
3830        &mut self,
3831        sketch: ObjectId,
3832        parallel: Parallel,
3833        new_ast: &mut ast::Node<ast::Program>,
3834    ) -> Result<AstNodeRef, KclError> {
3835        if parallel.lines.len() < 2 {
3836            return Err(KclError::refactor(format!(
3837                "Parallel constraint must have at least 2 lines, got {}",
3838                parallel.lines.len()
3839            )));
3840        };
3841
3842        let sketch_id = sketch;
3843
3844        let line_asts = parallel
3845            .lines
3846            .iter()
3847            .map(|line_id| {
3848                let line_object = self
3849                    .scene_graph
3850                    .objects
3851                    .get(line_id.0)
3852                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3853                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3854                    let kind = line_object.kind.human_friendly_kind_with_article();
3855                    return Err(KclError::refactor(format!(
3856                        "This constraint only works on Segments, but you selected {kind}"
3857                    )));
3858                };
3859                let Segment::Line(_) = line_segment else {
3860                    let kind = line_segment.human_friendly_kind_with_article();
3861                    return Err(KclError::refactor(format!(
3862                        "Only lines can be made parallel, but you selected {kind}"
3863                    )));
3864                };
3865
3866                get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)
3867            })
3868            .collect::<Result<Vec<_>, _>>()?;
3869
3870        let call_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3871            callee: ast::Node::no_src(ast_sketch2_name(LinesAtAngleKind::Parallel.to_function_name())),
3872            unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
3873                ast::ArrayExpression {
3874                    elements: line_asts,
3875                    digest: None,
3876                    non_code_meta: Default::default(),
3877                },
3878            )))),
3879            arguments: Default::default(),
3880            digest: None,
3881            non_code_meta: Default::default(),
3882        })));
3883
3884        let (sketch_block_ref, _) = self.mutate_ast(
3885            new_ast,
3886            sketch_id,
3887            AstMutateCommand::AddSketchBlockExprStmt { expr: call_ast },
3888        )?;
3889        Ok(sketch_block_ref)
3890    }
3891
3892    async fn add_perpendicular(
3893        &mut self,
3894        sketch: ObjectId,
3895        perpendicular: Perpendicular,
3896        new_ast: &mut ast::Node<ast::Program>,
3897    ) -> Result<AstNodeRef, KclError> {
3898        self.add_lines_at_angle_constraint(sketch, LinesAtAngleKind::Perpendicular, perpendicular.lines, new_ast)
3899            .await
3900    }
3901
3902    async fn add_lines_at_angle_constraint(
3903        &mut self,
3904        sketch: ObjectId,
3905        angle_kind: LinesAtAngleKind,
3906        lines: Vec<ObjectId>,
3907        new_ast: &mut ast::Node<ast::Program>,
3908    ) -> Result<AstNodeRef, KclError> {
3909        let &[line0_id, line1_id] = lines.as_slice() else {
3910            return Err(KclError::refactor(format!(
3911                "{} constraint must have exactly 2 lines, got {}",
3912                angle_kind.to_function_name(),
3913                lines.len()
3914            )));
3915        };
3916
3917        let sketch_id = sketch;
3918
3919        // Map the runtime objects back to variable names.
3920        let line0_object = self
3921            .scene_graph
3922            .objects
3923            .get(line0_id.0)
3924            .ok_or_else(|| KclError::refactor(format!("Line not found: {line0_id:?}")))?;
3925        let ObjectKind::Segment { segment: line0_segment } = &line0_object.kind else {
3926            let kind = line0_object.kind.human_friendly_kind_with_article();
3927            return Err(KclError::refactor(format!(
3928                "This constraint only works on Segments, but you selected {kind}"
3929            )));
3930        };
3931        let Segment::Line(_) = line0_segment else {
3932            return Err(KclError::refactor(format!(
3933                "Only lines can be made {}, but you selected {}",
3934                angle_kind.to_function_name(),
3935                line0_segment.human_friendly_kind_with_article(),
3936            )));
3937        };
3938        let line0_ast = get_or_insert_ast_reference(new_ast, &line0_object.source.clone(), LINE_VARIABLE, None)?;
3939
3940        let line1_object = self
3941            .scene_graph
3942            .objects
3943            .get(line1_id.0)
3944            .ok_or_else(|| KclError::refactor(format!("Line not found: {line1_id:?}")))?;
3945        let ObjectKind::Segment { segment: line1_segment } = &line1_object.kind else {
3946            let kind = line1_object.kind.human_friendly_kind_with_article();
3947            return Err(KclError::refactor(format!(
3948                "This constraint only works on Segments, but you selected {kind}"
3949            )));
3950        };
3951        let Segment::Line(_) = line1_segment else {
3952            return Err(KclError::refactor(format!(
3953                "Only lines can be made {}, but you selected {}",
3954                angle_kind.to_function_name(),
3955                line1_segment.human_friendly_kind_with_article(),
3956            )));
3957        };
3958        let line1_ast = get_or_insert_ast_reference(new_ast, &line1_object.source.clone(), LINE_VARIABLE, None)?;
3959
3960        // Create the parallel() or perpendicular() call.
3961        let call_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3962            callee: ast::Node::no_src(ast_sketch2_name(angle_kind.to_function_name())),
3963            unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
3964                ast::ArrayExpression {
3965                    elements: vec![line0_ast, line1_ast],
3966                    digest: None,
3967                    non_code_meta: Default::default(),
3968                },
3969            )))),
3970            arguments: Default::default(),
3971            digest: None,
3972            non_code_meta: Default::default(),
3973        })));
3974
3975        // Add the constraint to the AST of the sketch block.
3976        let (sketch_block_ref, _) = self.mutate_ast(
3977            new_ast,
3978            sketch_id,
3979            AstMutateCommand::AddSketchBlockExprStmt { expr: call_ast },
3980        )?;
3981        Ok(sketch_block_ref)
3982    }
3983
3984    async fn add_vertical(
3985        &mut self,
3986        sketch: ObjectId,
3987        vertical: Vertical,
3988        new_ast: &mut ast::Node<ast::Program>,
3989    ) -> Result<AstNodeRef, KclError> {
3990        let sketch_id = sketch;
3991
3992        let first_arg_ast = match vertical {
3993            Vertical::Line { line } => {
3994                // Map the runtime objects back to variable names.
3995                let line_object = self
3996                    .scene_graph
3997                    .objects
3998                    .get(line.0)
3999                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line:?}")))?;
4000                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4001                    let kind = line_object.kind.human_friendly_kind_with_article();
4002                    return Err(KclError::refactor(format!(
4003                        "This constraint only works on Segments, but you selected {kind}"
4004                    )));
4005                };
4006                let Segment::Line(_) = line_segment else {
4007                    return Err(KclError::refactor(format!(
4008                        "Only lines can be made vertical, but you selected {}",
4009                        line_segment.human_friendly_kind_with_article()
4010                    )));
4011                };
4012                get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)?
4013            }
4014            Vertical::Points { points } => {
4015                let point_asts = points
4016                    .iter()
4017                    .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
4018                    .collect::<Result<Vec<_>, _>>()?;
4019                ast::ArrayExpression::new(point_asts).into()
4020            }
4021        };
4022
4023        // Create the vertical() call using shared helper.
4024        let vertical_ast = create_vertical_ast(first_arg_ast);
4025
4026        // Add the line to the AST of the sketch block.
4027        let (sketch_block_ref, _) = self.mutate_ast(
4028            new_ast,
4029            sketch_id,
4030            AstMutateCommand::AddSketchBlockExprStmt { expr: vertical_ast },
4031        )?;
4032        Ok(sketch_block_ref)
4033    }
4034
4035    async fn execute_after_add_constraint(
4036        &mut self,
4037        ctx: &ExecutorContext,
4038        sketch_id: ObjectId,
4039        sketch_block_ref: AstNodeRef,
4040        new_ast: &mut ast::Node<ast::Program>,
4041    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
4042        // Convert to string source to create real source ranges.
4043        let new_source = source_from_ast(new_ast);
4044        // Parse the new KCL source.
4045        let (new_program, errors) = Program::parse(&new_source)
4046            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
4047        if !errors.is_empty() {
4048            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
4049                "Error parsing KCL source after adding constraint: {errors:?}"
4050            ))));
4051        }
4052        let Some(new_program) = new_program else {
4053            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
4054                "No AST produced after adding constraint".to_string(),
4055            )));
4056        };
4057        let constraint_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
4058            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
4059                "Source range of new constraint not found in sketch block: {sketch_block_ref:?}; {err:?}"
4060            )))
4061        })?;
4062
4063        // Truncate after the sketch block for mock execution.
4064        // Use a clone so we don't mutate new_program yet
4065        let mut truncated_program = new_program.clone();
4066        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
4067            .map_err(KclErrorWithOutputs::no_outputs)?;
4068
4069        // Execute - if this fails, we haven't modified self yet, so state is safe
4070        let outcome = ctx
4071            .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch_id))
4072            .await?;
4073
4074        let new_object_ids = {
4075            // Extract the constraint ID from the execution outcome using source_range_to_object
4076            let constraint_id = outcome
4077                .source_range_to_object
4078                .get(&constraint_node_ref.range)
4079                .copied()
4080                .ok_or_else(|| {
4081                    KclErrorWithOutputs::from_error_outcome(
4082                        KclError::refactor(format!("Source range of constraint not found: {constraint_node_ref:?}")),
4083                        outcome.clone(),
4084                    )
4085                })?;
4086            vec![constraint_id]
4087        };
4088
4089        // Only now, after all operations succeeded, update self.program
4090        // This ensures state is only modified if everything succeeds
4091        self.program = new_program;
4092
4093        // Uses MockConfig::default() which has freedom_analysis: true
4094        let outcome = self.update_state_after_exec(outcome, true);
4095
4096        let src_delta = SourceDelta { text: new_source };
4097        let scene_graph_delta = SceneGraphDelta {
4098            new_graph: self.scene_graph.clone(),
4099            invalidates_ids: false,
4100            new_objects: new_object_ids,
4101            exec_outcome: outcome,
4102        };
4103        Ok((src_delta, scene_graph_delta))
4104    }
4105
4106    // Find constraints that reference the given segments.
4107    fn segment_will_be_deleted(&self, segment_id: ObjectId, segment_ids_set: &AhashIndexSet<ObjectId>) -> bool {
4108        if segment_ids_set.contains(&segment_id) {
4109            return true;
4110        }
4111
4112        let Some(segment_object) = self.scene_graph.objects.get(segment_id.0) else {
4113            return false;
4114        };
4115        let ObjectKind::Segment { segment } = &segment_object.kind else {
4116            return false;
4117        };
4118        let Segment::Point(point) = segment else {
4119            return false;
4120        };
4121
4122        point.owner.is_some_and(|owner_id| segment_ids_set.contains(&owner_id))
4123    }
4124
4125    fn remaining_constraint_segments(
4126        &self,
4127        segments: &[ConstraintSegment],
4128        segment_ids_set: &AhashIndexSet<ObjectId>,
4129    ) -> Vec<ConstraintSegment> {
4130        segments
4131            .iter()
4132            .copied()
4133            .filter(|segment| match segment {
4134                ConstraintSegment::Origin(_) => true,
4135                ConstraintSegment::Segment(segment_id) => !self.segment_will_be_deleted(*segment_id, segment_ids_set),
4136            })
4137            .collect()
4138    }
4139
4140    fn find_referenced_constraints(
4141        &self,
4142        sketch_id: ObjectId,
4143        segment_ids_set: &AhashIndexSet<ObjectId>,
4144    ) -> Result<AhashIndexSet<ObjectId>, KclError> {
4145        // Look up the sketch.
4146        let sketch_object = self
4147            .scene_graph
4148            .objects
4149            .get(sketch_id.0)
4150            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch_id:?}")))?;
4151        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
4152            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
4153        };
4154        let mut constraint_ids_set = AhashIndexSet::default();
4155        for constraint_id in &sketch.constraints {
4156            let constraint_object = self
4157                .scene_graph
4158                .objects
4159                .get(constraint_id.0)
4160                .ok_or_else(|| KclError::refactor(format!("Constraint not found: {constraint_id:?}")))?;
4161            let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
4162                return Err(KclError::refactor(format!(
4163                    "Object is not a constraint, it is {}",
4164                    constraint_object.kind.human_friendly_kind_with_article()
4165                )));
4166            };
4167            let depends_on_segment = match constraint {
4168                Constraint::Coincident(c) => c
4169                    .segment_ids()
4170                    .any(|seg_id| self.segment_will_be_deleted(seg_id, segment_ids_set)),
4171                Constraint::Distance(d) => d
4172                    .point_ids()
4173                    .any(|pt_id| self.segment_will_be_deleted(pt_id, segment_ids_set)),
4174                Constraint::Fixed(fixed) => fixed
4175                    .points
4176                    .iter()
4177                    .any(|fixed_point| self.segment_will_be_deleted(fixed_point.point, segment_ids_set)),
4178                Constraint::Radius(r) => self.segment_will_be_deleted(r.arc, segment_ids_set),
4179                Constraint::Diameter(d) => self.segment_will_be_deleted(d.arc, segment_ids_set),
4180                Constraint::EqualRadius(equal_radius) => equal_radius
4181                    .input
4182                    .iter()
4183                    .any(|seg_id| self.segment_will_be_deleted(*seg_id, segment_ids_set)),
4184                Constraint::HorizontalDistance(d) => d
4185                    .point_ids()
4186                    .any(|pt_id| self.segment_will_be_deleted(pt_id, segment_ids_set)),
4187                Constraint::VerticalDistance(d) => d
4188                    .point_ids()
4189                    .any(|pt_id| self.segment_will_be_deleted(pt_id, segment_ids_set)),
4190                Constraint::Horizontal(h) => match h {
4191                    Horizontal::Line { line } => self.segment_will_be_deleted(*line, segment_ids_set),
4192                    Horizontal::Points { points } => points.iter().any(|point| match point {
4193                        ConstraintSegment::Segment(point) => self.segment_will_be_deleted(*point, segment_ids_set),
4194                        ConstraintSegment::Origin(_) => false,
4195                    }),
4196                },
4197                Constraint::Vertical(v) => match v {
4198                    Vertical::Line { line } => self.segment_will_be_deleted(*line, segment_ids_set),
4199                    Vertical::Points { points } => points.iter().any(|point| match point {
4200                        ConstraintSegment::Segment(point) => self.segment_will_be_deleted(*point, segment_ids_set),
4201                        ConstraintSegment::Origin(_) => false,
4202                    }),
4203                },
4204                Constraint::LinesEqualLength(lines_equal_length) => lines_equal_length
4205                    .lines
4206                    .iter()
4207                    .any(|line_id| self.segment_will_be_deleted(*line_id, segment_ids_set)),
4208                Constraint::Midpoint(midpoint) => {
4209                    self.segment_will_be_deleted(midpoint.segment, segment_ids_set)
4210                        || self.segment_will_be_deleted(midpoint.point, segment_ids_set)
4211                }
4212                Constraint::Parallel(parallel) => parallel
4213                    .lines
4214                    .iter()
4215                    .any(|line_id| self.segment_will_be_deleted(*line_id, segment_ids_set)),
4216                Constraint::Perpendicular(perpendicular) => perpendicular
4217                    .lines
4218                    .iter()
4219                    .any(|line_id| self.segment_will_be_deleted(*line_id, segment_ids_set)),
4220                Constraint::Angle(angle) => angle
4221                    .lines
4222                    .iter()
4223                    .any(|line_id| self.segment_will_be_deleted(*line_id, segment_ids_set)),
4224                Constraint::Symmetric(symmetric) => {
4225                    self.segment_will_be_deleted(symmetric.axis, segment_ids_set)
4226                        || symmetric
4227                            .input
4228                            .iter()
4229                            .any(|seg_id| self.segment_will_be_deleted(*seg_id, segment_ids_set))
4230                }
4231                Constraint::Tangent(tangent) => tangent
4232                    .input
4233                    .iter()
4234                    .any(|seg_id| self.segment_will_be_deleted(*seg_id, segment_ids_set)),
4235            };
4236            if depends_on_segment {
4237                constraint_ids_set.insert(*constraint_id);
4238            }
4239        }
4240        Ok(constraint_ids_set)
4241    }
4242
4243    fn update_state_after_exec(&mut self, outcome: ExecOutcome, freedom_analysis_ran: bool) -> ExecOutcome {
4244        let mut outcome = outcome;
4245        let mut new_objects = std::mem::take(&mut outcome.scene_objects);
4246
4247        if freedom_analysis_ran {
4248            // When freedom analysis ran, replace the cache entirely with new values
4249            // Don't merge with old values since IDs might have changed
4250            self.point_freedom_cache.clear();
4251            for new_obj in &new_objects {
4252                if let ObjectKind::Segment {
4253                    segment: crate::front::Segment::Point(point),
4254                } = &new_obj.kind
4255                {
4256                    self.point_freedom_cache.insert(new_obj.id, point.freedom);
4257                }
4258            }
4259            add_wall_and_cap_face_objects(&mut new_objects, &outcome.artifact_graph);
4260            // Objects are already correct from the analysis, just use them as-is
4261            self.scene_graph.objects = new_objects;
4262        } else {
4263            // When freedom analysis didn't run, preserve old values and merge
4264            // Before replacing objects, extract and store freedom values from old objects
4265            for old_obj in &self.scene_graph.objects {
4266                if let ObjectKind::Segment {
4267                    segment: crate::front::Segment::Point(point),
4268                } = &old_obj.kind
4269                {
4270                    self.point_freedom_cache.insert(old_obj.id, point.freedom);
4271                }
4272            }
4273
4274            // Update objects, preserving stored freedom values when new is Free (might be default)
4275            let mut updated_objects = Vec::with_capacity(new_objects.len());
4276            for new_obj in new_objects {
4277                let mut obj = new_obj;
4278                if let ObjectKind::Segment {
4279                    segment: crate::front::Segment::Point(point),
4280                } = &mut obj.kind
4281                {
4282                    let new_freedom = point.freedom;
4283                    // When freedom_analysis=false, new values are defaults (Free).
4284                    // Only preserve cached values when new is Free (indicating it's a default, not from analysis).
4285                    // If new is NOT Free, use the new value (it came from somewhere else, maybe conflict detection).
4286                    // Never preserve Conflict from cache - conflicts are transient and should only be set
4287                    // when there are actually unsatisfied constraints.
4288                    match new_freedom {
4289                        Freedom::Free => {
4290                            match self.point_freedom_cache.get(&obj.id).copied() {
4291                                Some(Freedom::Conflict) => {
4292                                    // Don't preserve Conflict - conflicts are transient
4293                                    // Keep it as Free
4294                                }
4295                                Some(Freedom::Fixed) => {
4296                                    // Preserve Fixed cached value
4297                                    point.freedom = Freedom::Fixed;
4298                                }
4299                                Some(Freedom::Free) => {
4300                                    // If stored is also Free, keep Free (no change needed)
4301                                }
4302                                None => {
4303                                    // If no cached value, keep Free (default)
4304                                }
4305                            }
4306                        }
4307                        Freedom::Fixed => {
4308                            // Use new value (already set)
4309                        }
4310                        Freedom::Conflict => {
4311                            // Use new value (already set)
4312                        }
4313                    }
4314                    // Store the new freedom value (even if it's Free, so we know it was set)
4315                    self.point_freedom_cache.insert(obj.id, point.freedom);
4316                }
4317                updated_objects.push(obj);
4318            }
4319
4320            add_wall_and_cap_face_objects(&mut updated_objects, &outcome.artifact_graph);
4321            self.scene_graph.objects = updated_objects;
4322        }
4323        outcome
4324    }
4325
4326    fn mutate_ast(
4327        &mut self,
4328        ast: &mut ast::Node<ast::Program>,
4329        object_id: ObjectId,
4330        command: AstMutateCommand,
4331    ) -> Result<(AstNodeRef, AstMutateCommandReturn), KclError> {
4332        let sketch_object = self
4333            .scene_graph
4334            .objects
4335            .get(object_id.0)
4336            .ok_or_else(|| KclError::refactor(format!("Object not found: {object_id:?}")))?;
4337        mutate_ast_node_by_source_ref(ast, &sketch_object.source, command)
4338    }
4339}
4340
4341fn sketch_block_ref_from_id(scene_graph: &SceneGraph, sketch_id: ObjectId) -> Result<AstNodeRef, KclError> {
4342    // Look up existing sketch.
4343    let sketch_object = scene_graph
4344        .objects
4345        .get(sketch_id.0)
4346        .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch_id:?}")))?;
4347    let ObjectKind::Sketch(_) = &sketch_object.kind else {
4348        return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
4349    };
4350    expect_single_node_ref(sketch_object)
4351}
4352
4353fn expect_single_node_ref(object: &Object) -> Result<AstNodeRef, KclError> {
4354    match &object.source {
4355        SourceRef::Simple { range, node_path } => Ok(AstNodeRef {
4356            range: *range,
4357            node_path: node_path.clone(),
4358        }),
4359        SourceRef::BackTrace { ranges } => {
4360            let [range] = ranges.as_slice() else {
4361                return Err(KclError::refactor(format!(
4362                    "Expected single location in SourceRef, got {}; ranges={ranges:#?}",
4363                    ranges.len()
4364                )));
4365            };
4366            Ok(AstNodeRef {
4367                range: range.0,
4368                node_path: range.1.clone(),
4369            })
4370        }
4371    }
4372}
4373
4374/// This is a deprecated fall-back implementation. Prefer
4375/// [`only_sketch_block()`] to avoid reliance on source ranges.
4376fn only_sketch_block_from_range(
4377    ast: &mut ast::Node<ast::Program>,
4378    sketch_block_range: SourceRange,
4379    edit_kind: ChangeKind,
4380) -> Result<(), KclError> {
4381    let r1 = sketch_block_range;
4382    let matches_range = |r2: SourceRange| -> bool {
4383        // We may have added items to the sketch block, so the end may not be an
4384        // exact match.
4385        match edit_kind {
4386            ChangeKind::Add => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() <= r2.end(),
4387            // For edit, we don't know whether it grew or shrank.
4388            ChangeKind::Edit => r1.module_id() == r2.module_id() && r1.start() == r2.start(),
4389            ChangeKind::Delete => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() >= r2.end(),
4390            // No edit should be an exact match.
4391            ChangeKind::None => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() == r2.end(),
4392        }
4393    };
4394    let mut found = false;
4395    for item in ast.body.iter_mut() {
4396        match item {
4397            ast::BodyItem::ImportStatement(_) => {}
4398            ast::BodyItem::ExpressionStatement(node) => {
4399                if matches_range(SourceRange::from(&*node))
4400                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.expression
4401                {
4402                    sketch_block.is_being_edited = true;
4403                    found = true;
4404                    break;
4405                }
4406            }
4407            ast::BodyItem::VariableDeclaration(node) => {
4408                if matches_range(SourceRange::from(&node.declaration.init))
4409                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.declaration.init
4410                {
4411                    sketch_block.is_being_edited = true;
4412                    found = true;
4413                    break;
4414                }
4415            }
4416            ast::BodyItem::TypeDeclaration(_) => {}
4417            ast::BodyItem::ReturnStatement(node) => {
4418                if matches_range(SourceRange::from(&node.argument))
4419                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.argument
4420                {
4421                    sketch_block.is_being_edited = true;
4422                    found = true;
4423                    break;
4424                }
4425            }
4426        }
4427    }
4428    if !found {
4429        return Err(KclError::refactor(format!(
4430            "Sketch block source range not found in AST: {sketch_block_range:?}, edit_kind={edit_kind:?}"
4431        )));
4432    }
4433
4434    Ok(())
4435}
4436
4437fn only_sketch_block(
4438    ast: &mut ast::Node<ast::Program>,
4439    sketch_block_ref: &AstNodeRef,
4440    edit_kind: ChangeKind,
4441) -> Result<(), KclError> {
4442    let Some(target_node_path) = &sketch_block_ref.node_path else {
4443        #[cfg(target_arch = "wasm32")]
4444        web_sys::console::warn_1(
4445            &format!(
4446                "only_sketch_block: target sketch block ref doesn't have node path; sketch_block_ref={:#?}, edit_kind={edit_kind:#?}",
4447                &sketch_block_ref
4448            )
4449            .into(),
4450        );
4451        return only_sketch_block_from_range(ast, sketch_block_ref.range, edit_kind);
4452    };
4453    let mut found = false;
4454    for item in ast.body.iter_mut() {
4455        match item {
4456            ast::BodyItem::ImportStatement(_) => {}
4457            ast::BodyItem::ExpressionStatement(node) => {
4458                // Check the statement.
4459                if let Some(node_path) = &node.node_path
4460                    && node_path == target_node_path
4461                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.expression
4462                {
4463                    sketch_block.is_being_edited = true;
4464                    found = true;
4465                    break;
4466                }
4467                // Check the expression.
4468                if let Some(node_path) = node.expression.node_path()
4469                    && node_path == target_node_path
4470                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.expression
4471                {
4472                    sketch_block.is_being_edited = true;
4473                    found = true;
4474                    break;
4475                }
4476            }
4477            ast::BodyItem::VariableDeclaration(node) => {
4478                if let Some(node_path) = node.declaration.init.node_path()
4479                    && node_path == target_node_path
4480                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.declaration.init
4481                {
4482                    sketch_block.is_being_edited = true;
4483                    found = true;
4484                    break;
4485                }
4486            }
4487            ast::BodyItem::TypeDeclaration(_) => {}
4488            ast::BodyItem::ReturnStatement(node) => {
4489                if let Some(node_path) = node.argument.node_path()
4490                    && node_path == target_node_path
4491                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.argument
4492                {
4493                    sketch_block.is_being_edited = true;
4494                    found = true;
4495                    break;
4496                }
4497            }
4498        }
4499    }
4500    if !found {
4501        return Err(KclError::refactor(format!(
4502            "Sketch block node path not found in AST: {sketch_block_ref:?}, edit_kind={edit_kind:?}"
4503        )));
4504    }
4505
4506    Ok(())
4507}
4508
4509fn sketch_on_ast_expr(
4510    ast: &mut ast::Node<ast::Program>,
4511    scene_graph: &SceneGraph,
4512    on: &Plane,
4513) -> Result<ast::Expr, KclError> {
4514    match on {
4515        Plane::Default(name) => Ok(default_plane_ast_expr(*name)),
4516        Plane::Object(object_id) => {
4517            let on_object = scene_graph
4518                .objects
4519                .get(object_id.0)
4520                .ok_or_else(|| KclError::refactor(format!("Sketch plane object not found: {object_id:?}")))?;
4521            if let Some(face_expr) = sketch_face_of_scene_object_ast_expr(ast, on_object)? {
4522                return Ok(face_expr);
4523            }
4524            get_or_insert_ast_reference(ast, &on_object.source, "plane", None)
4525        }
4526    }
4527}
4528
4529fn sketch_face_of_scene_object_ast_expr(
4530    ast: &mut ast::Node<ast::Program>,
4531    on_object: &crate::front::Object,
4532) -> Result<Option<ast::Expr>, KclError> {
4533    let SourceRef::BackTrace { ranges } = &on_object.source else {
4534        return Ok(None);
4535    };
4536
4537    match &on_object.kind {
4538        ObjectKind::Wall(_) => {
4539            let [sweep_range, segment_range] = ranges.as_slice() else {
4540                return Err(KclError::refactor(format!(
4541                    "Expected wall source metadata to have 2 ranges, got {}; artifact_id={:?}",
4542                    ranges.len(),
4543                    on_object.artifact_id
4544                )));
4545            };
4546            let sweep_ref = get_or_insert_ast_reference(
4547                ast,
4548                &SourceRef::Simple {
4549                    range: sweep_range.0,
4550                    node_path: sweep_range.1.clone(),
4551                },
4552                "solid",
4553                None,
4554            )?;
4555            let ast::Expr::Name(solid_name_expr) = sweep_ref else {
4556                return Err(KclError::refactor(format!(
4557                    "Could not resolve sweep reference for selected wall: artifact_id={:?}",
4558                    on_object.artifact_id
4559                )));
4560            };
4561            let solid_name = solid_name_expr.name.name.clone();
4562            let solid_expr = ast_name_expr(solid_name.clone());
4563            let segment_ref = get_or_insert_ast_reference(
4564                ast,
4565                &SourceRef::Simple {
4566                    range: segment_range.0,
4567                    node_path: segment_range.1.clone(),
4568                },
4569                LINE_VARIABLE,
4570                None,
4571            )?;
4572
4573            let face_expr = if let Some(region_name) = region_name_from_sweep_variable(ast, &solid_name) {
4574                let ast::Expr::Name(segment_name_expr) = segment_ref else {
4575                    return Err(KclError::refactor(format!(
4576                        "Could not resolve source segment reference for selected region wall: artifact_id={:?}",
4577                        on_object.artifact_id
4578                    )));
4579                };
4580                create_member_expression(
4581                    create_member_expression(ast_name_expr(region_name), "tags"),
4582                    &segment_name_expr.name.name,
4583                )
4584            } else {
4585                segment_ref
4586            };
4587
4588            Ok(Some(create_face_of_ast(solid_expr, face_expr)))
4589        }
4590        ObjectKind::Cap(cap) => {
4591            let [range] = ranges.as_slice() else {
4592                return Err(KclError::refactor(format!(
4593                    "Expected cap source metadata to have 1 range, got {}; artifact_id={:?}",
4594                    ranges.len(),
4595                    on_object.artifact_id
4596                )));
4597            };
4598            let sweep_ref = get_or_insert_ast_reference(
4599                ast,
4600                &SourceRef::Simple {
4601                    range: range.0,
4602                    node_path: range.1.clone(),
4603                },
4604                "solid",
4605                None,
4606            )?;
4607            let ast::Expr::Name(solid_name_expr) = sweep_ref else {
4608                return Err(KclError::refactor(format!(
4609                    "Could not resolve sweep reference for selected cap: artifact_id={:?}",
4610                    on_object.artifact_id
4611                )));
4612            };
4613            let solid_expr = ast_name_expr(solid_name_expr.name.name.clone());
4614            // TODO: change this to explicit tag references with tagStart/tagEnd mutations
4615            let face_expr = match cap.kind {
4616                crate::frontend::api::CapKind::Start => ast_name_expr("START".to_owned()),
4617                crate::frontend::api::CapKind::End => ast_name_expr("END".to_owned()),
4618            };
4619
4620            Ok(Some(create_face_of_ast(solid_expr, face_expr)))
4621        }
4622        _ => Ok(None),
4623    }
4624}
4625
4626fn add_wall_and_cap_face_objects(scene_objects: &mut Vec<crate::front::Object>, artifact_graph: &ArtifactGraph) {
4627    let mut existing_artifact_ids = scene_objects
4628        .iter()
4629        .map(|object| object.artifact_id)
4630        .collect::<HashSet<_>>();
4631
4632    for artifact in artifact_graph.values() {
4633        match artifact {
4634            Artifact::Wall(wall) => {
4635                if existing_artifact_ids.contains(&wall.id) {
4636                    continue;
4637                }
4638
4639                let Some(segment) = artifact_graph.get(&wall.seg_id).and_then(|artifact| match artifact {
4640                    Artifact::Segment(segment) => Some(segment),
4641                    _ => None,
4642                }) else {
4643                    continue;
4644                };
4645                let Some(sweep) = artifact_graph.get(&wall.sweep_id).and_then(|artifact| match artifact {
4646                    Artifact::Sweep(sweep) => Some(sweep),
4647                    _ => None,
4648                }) else {
4649                    continue;
4650                };
4651                let source_segment = segment
4652                    .original_seg_id
4653                    .and_then(|original_seg_id| artifact_graph.get(&original_seg_id))
4654                    .and_then(|artifact| match artifact {
4655                        Artifact::Segment(segment) => Some(segment),
4656                        _ => None,
4657                    })
4658                    .unwrap_or(segment);
4659                let id = ObjectId(scene_objects.len());
4660                scene_objects.push(crate::front::Object {
4661                    id,
4662                    kind: ObjectKind::Wall(crate::frontend::api::Wall { id }),
4663                    label: Default::default(),
4664                    comments: Default::default(),
4665                    artifact_id: wall.id,
4666                    source: SourceRef::BackTrace {
4667                        ranges: vec![
4668                            (sweep.code_ref.range, Some(sweep.code_ref.node_path.clone())),
4669                            (
4670                                source_segment.code_ref.range,
4671                                Some(source_segment.code_ref.node_path.clone()),
4672                            ),
4673                        ],
4674                    },
4675                });
4676                existing_artifact_ids.insert(wall.id);
4677            }
4678            Artifact::Cap(cap) => {
4679                if existing_artifact_ids.contains(&cap.id) {
4680                    continue;
4681                }
4682
4683                let Some(sweep) = artifact_graph.get(&cap.sweep_id).and_then(|artifact| match artifact {
4684                    Artifact::Sweep(sweep) => Some(sweep),
4685                    _ => None,
4686                }) else {
4687                    continue;
4688                };
4689                let id = ObjectId(scene_objects.len());
4690                let kind = match cap.sub_type {
4691                    CapSubType::Start => crate::frontend::api::CapKind::Start,
4692                    CapSubType::End => crate::frontend::api::CapKind::End,
4693                };
4694                scene_objects.push(crate::front::Object {
4695                    id,
4696                    kind: ObjectKind::Cap(crate::frontend::api::Cap { id, kind }),
4697                    label: Default::default(),
4698                    comments: Default::default(),
4699                    artifact_id: cap.id,
4700                    source: SourceRef::BackTrace {
4701                        ranges: vec![(sweep.code_ref.range, Some(sweep.code_ref.node_path.clone()))],
4702                    },
4703                });
4704                existing_artifact_ids.insert(cap.id);
4705            }
4706            _ => {}
4707        }
4708    }
4709}
4710
4711fn default_plane_ast_expr(name: crate::engine::PlaneName) -> ast::Expr {
4712    use crate::engine::PlaneName;
4713
4714    match name {
4715        PlaneName::Xy => ast_name_expr("XY".to_owned()),
4716        PlaneName::Xz => ast_name_expr("XZ".to_owned()),
4717        PlaneName::Yz => ast_name_expr("YZ".to_owned()),
4718        PlaneName::NegXy => negated_plane_ast_expr("XY"),
4719        PlaneName::NegXz => negated_plane_ast_expr("XZ"),
4720        PlaneName::NegYz => negated_plane_ast_expr("YZ"),
4721    }
4722}
4723
4724fn negated_plane_ast_expr(name: &str) -> ast::Expr {
4725    ast::Expr::UnaryExpression(Box::new(ast::UnaryExpression::new(
4726        ast::UnaryOperator::Neg,
4727        ast::BinaryPart::Name(Box::new(ast_name(name.to_owned()))),
4728    )))
4729}
4730
4731fn create_face_of_ast(solid_expr: ast::Expr, face_expr: ast::Expr) -> ast::Expr {
4732    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
4733        callee: ast::Node::no_src(ast_sketch2_name("faceOf")),
4734        unlabeled: Some(solid_expr),
4735        arguments: vec![ast::LabeledArg {
4736            label: Some(ast::Identifier::new("face")),
4737            arg: face_expr,
4738        }],
4739        digest: None,
4740        non_code_meta: Default::default(),
4741    })))
4742}
4743
4744fn region_name_from_sweep_variable(ast: &ast::Node<ast::Program>, sweep_variable_name: &str) -> Option<String> {
4745    let ast::Definition::Variable(sweep_decl) = ast.get_variable(sweep_variable_name)? else {
4746        return None;
4747    };
4748    let ast::Expr::CallExpressionKw(sweep_call) = &sweep_decl.init else {
4749        return None;
4750    };
4751    if !matches!(
4752        sweep_call.callee.name.name.as_str(),
4753        "extrude" | "revolve" | "sweep" | "loft"
4754    ) {
4755        return None;
4756    }
4757    let ast::Expr::Name(region_name_expr) = sweep_call.unlabeled.as_ref()? else {
4758        return None;
4759    };
4760    let candidate = region_name_expr.name.name.clone();
4761    let ast::Definition::Variable(region_decl) = ast.get_variable(&candidate)? else {
4762        return None;
4763    };
4764    let ast::Expr::CallExpressionKw(region_call) = &region_decl.init else {
4765        return None;
4766    };
4767    if region_call.callee.name.name != "region" {
4768        return None;
4769    }
4770    Some(candidate)
4771}
4772
4773/// Return the AST expression referencing the variable at the given source ref.
4774/// If no such variable exists, insert a new variable declaration with the given
4775/// prefix.
4776///
4777/// This may return a complex expression referencing properties of the variable
4778/// (e.g., `line1.start`).
4779fn get_or_insert_ast_reference(
4780    ast: &mut ast::Node<ast::Program>,
4781    source_ref: &SourceRef,
4782    prefix: &str,
4783    property: Option<&str>,
4784) -> Result<ast::Expr, KclError> {
4785    let command = AstMutateCommand::AddVariableDeclaration {
4786        prefix: prefix.to_owned(),
4787    };
4788    let (_, ret) = mutate_ast_node_by_source_ref(ast, source_ref, command)?;
4789    let AstMutateCommandReturn::Name(var_name) = ret else {
4790        return Err(KclError::refactor(
4791            "Expected variable name returned from AddVariableDeclaration".to_owned(),
4792        ));
4793    };
4794    let var_expr = ast::Expr::Name(Box::new(ast::Name::new(&var_name)));
4795    let Some(property) = property else {
4796        // No property; just return the variable name.
4797        return Ok(var_expr);
4798    };
4799
4800    Ok(create_member_expression(var_expr, property))
4801}
4802
4803fn mutate_ast_node_by_source_ref(
4804    ast: &mut ast::Node<ast::Program>,
4805    source_ref: &SourceRef,
4806    command: AstMutateCommand,
4807) -> Result<(AstNodeRef, AstMutateCommandReturn), KclError> {
4808    let (source_range, node_path) = match source_ref {
4809        SourceRef::Simple { range, node_path } => (*range, node_path.clone()),
4810        SourceRef::BackTrace { ranges } => {
4811            let [range] = ranges.as_slice() else {
4812                return Err(KclError::refactor(format!(
4813                    "Expected single source ref, got {}; ranges={ranges:#?}",
4814                    ranges.len(),
4815                )));
4816            };
4817            (range.0, range.1.clone())
4818        }
4819    };
4820    let mut context = AstMutateContext {
4821        source_range,
4822        node_path,
4823        command,
4824        defined_names_stack: Default::default(),
4825    };
4826    let control = dfs_mut(ast, &mut context);
4827    match control {
4828        ControlFlow::Continue(_) => Err(KclError::refactor(
4829            "Could not find the KCL source for this edit. Try reloading the app, or update from code.".to_owned(),
4830        )),
4831        ControlFlow::Break(break_value) => break_value,
4832    }
4833}
4834
4835#[derive(Debug)]
4836struct AstMutateContext {
4837    source_range: SourceRange,
4838    node_path: Option<ast::NodePath>,
4839    command: AstMutateCommand,
4840    defined_names_stack: Vec<HashSet<String>>,
4841}
4842
4843#[derive(Debug)]
4844#[allow(clippy::large_enum_variant)]
4845enum AstMutateCommand {
4846    /// Add an expression statement to the sketch block.
4847    AddSketchBlockExprStmt {
4848        expr: ast::Expr,
4849    },
4850    /// Add a variable declaration to the sketch block (e.g. `line1 = line(...)`).
4851    AddSketchBlockVarDecl {
4852        prefix: String,
4853        expr: ast::Expr,
4854    },
4855    AddVariableDeclaration {
4856        prefix: String,
4857    },
4858    EditPoint {
4859        at: ast::Expr,
4860    },
4861    EditLine {
4862        start: ast::Expr,
4863        end: ast::Expr,
4864        construction: Option<bool>,
4865    },
4866    EditArc {
4867        start: ast::Expr,
4868        end: ast::Expr,
4869        center: ast::Expr,
4870        construction: Option<bool>,
4871    },
4872    EditCircle {
4873        start: ast::Expr,
4874        center: ast::Expr,
4875        construction: Option<bool>,
4876    },
4877    EditConstraintValue {
4878        value: ast::BinaryPart,
4879    },
4880    EditDistanceConstraintLabelPosition {
4881        label_position: ast::Expr,
4882    },
4883    EditCallUnlabeled {
4884        arg: ast::Expr,
4885    },
4886    EditVarInitialValue {
4887        value: Number,
4888    },
4889    DeleteNode,
4890}
4891
4892impl AstMutateCommand {
4893    fn needs_defined_names_stack(&self) -> bool {
4894        matches!(
4895            self,
4896            AstMutateCommand::AddSketchBlockVarDecl { .. } | AstMutateCommand::AddVariableDeclaration { .. }
4897        )
4898    }
4899}
4900
4901#[derive(Debug)]
4902enum AstMutateCommandReturn {
4903    None,
4904    Name(String),
4905}
4906
4907#[derive(Debug, Clone)]
4908struct AstNodeRef {
4909    range: SourceRange,
4910    node_path: Option<ast::NodePath>,
4911}
4912
4913impl<T> From<&ast::Node<T>> for AstNodeRef {
4914    fn from(value: &ast::Node<T>) -> Self {
4915        AstNodeRef {
4916            range: value.into(),
4917            node_path: value.node_path.clone(),
4918        }
4919    }
4920}
4921
4922impl From<&ast::BodyItem> for AstNodeRef {
4923    fn from(value: &ast::BodyItem) -> Self {
4924        match value {
4925            ast::BodyItem::ImportStatement(node) => AstNodeRef {
4926                range: node.into(),
4927                node_path: node.node_path.clone(),
4928            },
4929            ast::BodyItem::ExpressionStatement(node) => AstNodeRef {
4930                range: node.into(),
4931                node_path: node.node_path.clone(),
4932            },
4933            ast::BodyItem::VariableDeclaration(node) => AstNodeRef {
4934                range: node.into(),
4935                node_path: node.node_path.clone(),
4936            },
4937            ast::BodyItem::TypeDeclaration(node) => AstNodeRef {
4938                range: node.into(),
4939                node_path: node.node_path.clone(),
4940            },
4941            ast::BodyItem::ReturnStatement(node) => AstNodeRef {
4942                range: node.into(),
4943                node_path: node.node_path.clone(),
4944            },
4945        }
4946    }
4947}
4948
4949impl From<&ast::Expr> for AstNodeRef {
4950    fn from(value: &ast::Expr) -> Self {
4951        AstNodeRef {
4952            range: SourceRange::from(value),
4953            node_path: value.node_path().cloned(),
4954        }
4955    }
4956}
4957
4958impl From<&AstMutateContext> for AstNodeRef {
4959    fn from(value: &AstMutateContext) -> Self {
4960        AstNodeRef {
4961            range: value.source_range,
4962            node_path: value.node_path.clone(),
4963        }
4964    }
4965}
4966
4967impl TryFrom<&NodeMut<'_>> for AstNodeRef {
4968    type Error = crate::walk::AstNodeError;
4969
4970    fn try_from(value: &NodeMut<'_>) -> Result<Self, Self::Error> {
4971        Ok(AstNodeRef {
4972            range: SourceRange::try_from(value)?,
4973            node_path: value.try_into()?,
4974        })
4975    }
4976}
4977
4978impl From<AstNodeRef> for SourceRange {
4979    fn from(value: AstNodeRef) -> Self {
4980        value.range
4981    }
4982}
4983
4984impl Visitor for AstMutateContext {
4985    type Break = Result<(AstNodeRef, AstMutateCommandReturn), KclError>;
4986    type Continue = ();
4987
4988    fn visit(&mut self, node: NodeMut<'_>) -> TraversalReturn<Self::Break, Self::Continue> {
4989        filter_and_process(self, node)
4990    }
4991
4992    fn finish(&mut self, node: NodeMut<'_>) {
4993        match &node {
4994            NodeMut::Program(_) | NodeMut::SketchBlock(_) => {
4995                self.defined_names_stack.pop();
4996            }
4997            _ => {}
4998        }
4999    }
5000}
5001
5002fn filter_and_process(
5003    ctx: &mut AstMutateContext,
5004    node: NodeMut,
5005) -> TraversalReturn<Result<(AstNodeRef, AstMutateCommandReturn), KclError>> {
5006    let Ok(node_range) = SourceRange::try_from(&node) else {
5007        // Nodes that can't be converted to a range aren't interesting.
5008        return TraversalReturn::new_continue(());
5009    };
5010    // If we're adding a variable declaration, we need to look at variable
5011    // declaration expressions to see if it already has a variable, before
5012    // continuing. The variable declaration's source range won't match the
5013    // target; its init expression will.
5014    if let NodeMut::VariableDeclaration(var_decl) = &node {
5015        let expr_range = SourceRange::from(&var_decl.declaration.init);
5016        let expr_node_path = var_decl.declaration.init.node_path();
5017        if source_ref_matches(ctx, expr_range, expr_node_path) {
5018            if let AstMutateCommand::AddVariableDeclaration { .. } = &ctx.command {
5019                // We found the variable declaration expression. It doesn't need
5020                // to be added.
5021                return TraversalReturn::new_break(Ok((
5022                    AstNodeRef::from(&**var_decl),
5023                    AstMutateCommandReturn::Name(var_decl.name().to_owned()),
5024                )));
5025            }
5026            if let AstMutateCommand::DeleteNode = &ctx.command {
5027                // We found the variable declaration. Delete the variable along
5028                // with the segment.
5029                return TraversalReturn {
5030                    mutate_body_item: MutateBodyItem::Delete,
5031                    control_flow: ControlFlow::Break(Ok((AstNodeRef::from(&*ctx), AstMutateCommandReturn::None))),
5032                };
5033            }
5034        }
5035    }
5036    // Similar thing with expression statement. We need to look at the
5037    // expression inside it.
5038    if let NodeMut::ExpressionStatement(expr_stmt) = &node {
5039        let expr_range = SourceRange::from(&expr_stmt.expression);
5040        let expr_node_path = expr_stmt.expression.node_path();
5041        if source_ref_matches(ctx, expr_range, expr_node_path) {
5042            if let AstMutateCommand::AddVariableDeclaration { .. } = &ctx.command {
5043                // We found the node wrapped in an expression statement. Process
5044                // the statement.
5045                let Ok(node_ref) = AstNodeRef::try_from(&node) else {
5046                    return TraversalReturn::new_continue(());
5047                };
5048                return process(ctx, node).map_break(|result| result.map(|cmd_return| (node_ref, cmd_return)));
5049            }
5050            if let AstMutateCommand::DeleteNode = &ctx.command {
5051                // We found the node wrapped in an expression statement. Delete
5052                // the whole statement.
5053                return TraversalReturn {
5054                    mutate_body_item: MutateBodyItem::Delete,
5055                    control_flow: ControlFlow::Break(Ok((AstNodeRef::from(&*ctx), AstMutateCommandReturn::None))),
5056                };
5057            }
5058        }
5059    }
5060
5061    if ctx.command.needs_defined_names_stack() {
5062        if let NodeMut::Program(program) = &node {
5063            ctx.defined_names_stack.push(find_defined_names(*program));
5064        } else if let NodeMut::SketchBlock(block) = &node {
5065            ctx.defined_names_stack.push(find_defined_names(&block.body));
5066        }
5067    }
5068
5069    // Make sure the node matches the source ref.
5070    let node_path = <Option<ast::NodePath>>::try_from(&node).ok().flatten();
5071    if !source_ref_matches(ctx, node_range, node_path.as_ref()) {
5072        return TraversalReturn::new_continue(());
5073    }
5074    let Ok(node_ref) = AstNodeRef::try_from(&node) else {
5075        return TraversalReturn::new_continue(());
5076    };
5077    process(ctx, node).map_break(|result| result.map(|cmd_return| (node_ref, cmd_return)))
5078}
5079
5080fn source_ref_matches(ctx: &AstMutateContext, node_range: SourceRange, node_path: Option<&ast::NodePath>) -> bool {
5081    match &ctx.node_path {
5082        Some(target) => Some(target) == node_path,
5083        None => node_range == ctx.source_range,
5084    }
5085}
5086
5087fn process(ctx: &AstMutateContext, node: NodeMut) -> TraversalReturn<Result<AstMutateCommandReturn, KclError>> {
5088    match &ctx.command {
5089        AstMutateCommand::AddSketchBlockExprStmt { expr } => {
5090            if let NodeMut::SketchBlock(sketch_block) = node {
5091                sketch_block
5092                    .body
5093                    .items
5094                    .push(ast::BodyItem::ExpressionStatement(ast::Node {
5095                        inner: ast::ExpressionStatement {
5096                            expression: expr.clone(),
5097                            digest: None,
5098                        },
5099                        start: Default::default(),
5100                        end: Default::default(),
5101                        module_id: Default::default(),
5102                        node_path: None,
5103                        outer_attrs: Default::default(),
5104                        pre_comments: Default::default(),
5105                        comment_start: Default::default(),
5106                    }));
5107                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5108            }
5109        }
5110        AstMutateCommand::AddSketchBlockVarDecl { prefix, expr } => {
5111            if let NodeMut::SketchBlock(sketch_block) = node {
5112                let empty_defined_names = HashSet::new();
5113                let defined_names = ctx.defined_names_stack.last().unwrap_or(&empty_defined_names);
5114                let Ok(name) = next_free_name(prefix, defined_names) else {
5115                    return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5116                };
5117                sketch_block
5118                    .body
5119                    .items
5120                    .push(ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(
5121                        ast::VariableDeclaration::new(
5122                            ast::VariableDeclarator::new(&name, expr.clone()),
5123                            ast::ItemVisibility::Default,
5124                            ast::VariableKind::Const,
5125                        ),
5126                    ))));
5127                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::Name(name)));
5128            }
5129        }
5130        AstMutateCommand::AddVariableDeclaration { prefix } => {
5131            if let NodeMut::VariableDeclaration(inner) = node {
5132                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::Name(inner.name().to_owned())));
5133            }
5134            if let NodeMut::ExpressionStatement(expr_stmt) = node {
5135                let empty_defined_names = HashSet::new();
5136                let defined_names = ctx.defined_names_stack.last().unwrap_or(&empty_defined_names);
5137                let Ok(name) = next_free_name(prefix, defined_names) else {
5138                    // TODO: Return an error instead?
5139                    return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5140                };
5141                let mutate_node =
5142                    ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(ast::VariableDeclaration::new(
5143                        ast::VariableDeclarator::new(&name, expr_stmt.expression.clone()),
5144                        ast::ItemVisibility::Default,
5145                        ast::VariableKind::Const,
5146                    ))));
5147                return TraversalReturn {
5148                    mutate_body_item: MutateBodyItem::Mutate(Box::new(mutate_node)),
5149                    control_flow: ControlFlow::Break(Ok(AstMutateCommandReturn::Name(name))),
5150                };
5151            }
5152        }
5153        AstMutateCommand::EditPoint { at } => {
5154            if let NodeMut::CallExpressionKw(call) = node {
5155                if call.callee.name.name != POINT_FN {
5156                    return TraversalReturn::new_continue(());
5157                }
5158                // Update the arguments.
5159                for labeled_arg in &mut call.arguments {
5160                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(POINT_AT_PARAM) {
5161                        labeled_arg.arg = at.clone();
5162                    }
5163                }
5164                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5165            }
5166        }
5167        AstMutateCommand::EditLine {
5168            start,
5169            end,
5170            construction,
5171        } => {
5172            if let NodeMut::CallExpressionKw(call) = node {
5173                if call.callee.name.name != LINE_FN {
5174                    return TraversalReturn::new_continue(());
5175                }
5176                // Update the arguments.
5177                for labeled_arg in &mut call.arguments {
5178                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(LINE_START_PARAM) {
5179                        labeled_arg.arg = start.clone();
5180                    }
5181                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(LINE_END_PARAM) {
5182                        labeled_arg.arg = end.clone();
5183                    }
5184                }
5185                // Handle construction kwarg
5186                if let Some(construction_value) = construction {
5187                    let construction_exists = call
5188                        .arguments
5189                        .iter()
5190                        .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
5191                    if *construction_value {
5192                        // Add or update construction=true
5193                        if construction_exists {
5194                            // Update existing construction kwarg
5195                            for labeled_arg in &mut call.arguments {
5196                                if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
5197                                    labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
5198                                        value: ast::LiteralValue::Bool(true),
5199                                        raw: "true".to_string(),
5200                                        digest: None,
5201                                    })));
5202                                }
5203                            }
5204                        } else {
5205                            // Add new construction kwarg
5206                            call.arguments.push(ast::LabeledArg {
5207                                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
5208                                arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
5209                                    value: ast::LiteralValue::Bool(true),
5210                                    raw: "true".to_string(),
5211                                    digest: None,
5212                                }))),
5213                            });
5214                        }
5215                    } else {
5216                        // Remove construction kwarg if it exists
5217                        call.arguments
5218                            .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
5219                    }
5220                }
5221                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5222            }
5223        }
5224        AstMutateCommand::EditArc {
5225            start,
5226            end,
5227            center,
5228            construction,
5229        } => {
5230            if let NodeMut::CallExpressionKw(call) = node {
5231                if call.callee.name.name != ARC_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(ARC_START_PARAM) {
5237                        labeled_arg.arg = start.clone();
5238                    }
5239                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_END_PARAM) {
5240                        labeled_arg.arg = end.clone();
5241                    }
5242                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_CENTER_PARAM) {
5243                        labeled_arg.arg = center.clone();
5244                    }
5245                }
5246                // Handle construction kwarg
5247                if let Some(construction_value) = construction {
5248                    let construction_exists = call
5249                        .arguments
5250                        .iter()
5251                        .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
5252                    if *construction_value {
5253                        // Add or update construction=true
5254                        if construction_exists {
5255                            // Update existing construction kwarg
5256                            for labeled_arg in &mut call.arguments {
5257                                if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
5258                                    labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
5259                                        value: ast::LiteralValue::Bool(true),
5260                                        raw: "true".to_string(),
5261                                        digest: None,
5262                                    })));
5263                                }
5264                            }
5265                        } else {
5266                            // Add new construction kwarg
5267                            call.arguments.push(ast::LabeledArg {
5268                                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
5269                                arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
5270                                    value: ast::LiteralValue::Bool(true),
5271                                    raw: "true".to_string(),
5272                                    digest: None,
5273                                }))),
5274                            });
5275                        }
5276                    } else {
5277                        // Remove construction kwarg if it exists
5278                        call.arguments
5279                            .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
5280                    }
5281                }
5282                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5283            }
5284        }
5285        AstMutateCommand::EditCircle {
5286            start,
5287            center,
5288            construction,
5289        } => {
5290            if let NodeMut::CallExpressionKw(call) = node {
5291                if call.callee.name.name != CIRCLE_FN {
5292                    return TraversalReturn::new_continue(());
5293                }
5294                // Update the arguments.
5295                for labeled_arg in &mut call.arguments {
5296                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CIRCLE_START_PARAM) {
5297                        labeled_arg.arg = start.clone();
5298                    }
5299                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CIRCLE_CENTER_PARAM) {
5300                        labeled_arg.arg = center.clone();
5301                    }
5302                }
5303                // Handle construction kwarg
5304                if let Some(construction_value) = construction {
5305                    let construction_exists = call
5306                        .arguments
5307                        .iter()
5308                        .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
5309                    if *construction_value {
5310                        if construction_exists {
5311                            for labeled_arg in &mut call.arguments {
5312                                if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
5313                                    labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
5314                                        value: ast::LiteralValue::Bool(true),
5315                                        raw: "true".to_string(),
5316                                        digest: None,
5317                                    })));
5318                                }
5319                            }
5320                        } else {
5321                            call.arguments.push(ast::LabeledArg {
5322                                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
5323                                arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
5324                                    value: ast::LiteralValue::Bool(true),
5325                                    raw: "true".to_string(),
5326                                    digest: None,
5327                                }))),
5328                            });
5329                        }
5330                    } else {
5331                        call.arguments
5332                            .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
5333                    }
5334                }
5335                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5336            }
5337        }
5338        AstMutateCommand::EditConstraintValue { value } => {
5339            if let NodeMut::BinaryExpression(binary_expr) = node {
5340                let left_is_constraint = matches!(
5341                    &binary_expr.left,
5342                    ast::BinaryPart::CallExpressionKw(call)
5343                        if matches!(
5344                            call.callee.name.name.as_str(),
5345                            DISTANCE_FN | HORIZONTAL_DISTANCE_FN | VERTICAL_DISTANCE_FN | RADIUS_FN | DIAMETER_FN | ANGLE_FN
5346                        )
5347                );
5348                if left_is_constraint {
5349                    binary_expr.right = value.clone();
5350                } else {
5351                    binary_expr.left = value.clone();
5352                }
5353
5354                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5355            }
5356        }
5357        AstMutateCommand::EditDistanceConstraintLabelPosition { label_position } => {
5358            if let NodeMut::BinaryExpression(binary_expr) = node {
5359                let ast::BinaryPart::CallExpressionKw(call) = &mut binary_expr.left else {
5360                    return TraversalReturn::new_continue(());
5361                };
5362                if !matches!(
5363                    call.callee.name.name.as_str(),
5364                    DISTANCE_FN | HORIZONTAL_DISTANCE_FN | VERTICAL_DISTANCE_FN | RADIUS_FN | DIAMETER_FN
5365                ) {
5366                    return TraversalReturn::new_continue(());
5367                }
5368
5369                if let Some(label_arg) = call
5370                    .arguments
5371                    .iter_mut()
5372                    .find(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(LABEL_POSITION_PARAM))
5373                {
5374                    label_arg.arg = label_position.clone();
5375                } else {
5376                    call.arguments.push(ast::LabeledArg {
5377                        label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
5378                        arg: label_position.clone(),
5379                    });
5380                }
5381
5382                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5383            }
5384        }
5385        AstMutateCommand::EditCallUnlabeled { arg } => {
5386            if let NodeMut::CallExpressionKw(call) = node {
5387                call.unlabeled = Some(arg.clone());
5388                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5389            }
5390        }
5391        AstMutateCommand::EditVarInitialValue { value } => {
5392            if let NodeMut::NumericLiteral(numeric_literal) = node {
5393                // Update the initial value.
5394                let Ok(literal) = to_source_number(*value) else {
5395                    return TraversalReturn::new_break(Err(KclError::refactor(format!(
5396                        "Could not convert number to AST literal: {:?}",
5397                        *value
5398                    ))));
5399                };
5400                *numeric_literal = ast::Node::no_src(literal);
5401                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5402            }
5403        }
5404        AstMutateCommand::DeleteNode => {
5405            return TraversalReturn {
5406                mutate_body_item: MutateBodyItem::Delete,
5407                control_flow: ControlFlow::Break(Ok(AstMutateCommandReturn::None)),
5408            };
5409        }
5410    }
5411    TraversalReturn::new_continue(())
5412}
5413
5414struct FindSketchBlockSourceRange {
5415    /// The source range of the sketch block before mutation.
5416    target_before_mutation: SourceRange,
5417    /// The source range of the sketch block's last body item after mutation. We
5418    /// need to use a [Cell] since the [crate::walk::Visitor] trait requires a
5419    /// shared reference.
5420    found: Cell<Option<AstNodeRef>>,
5421}
5422
5423impl<'a> crate::walk::Visitor<'a> for &FindSketchBlockSourceRange {
5424    type Error = crate::front::Error;
5425
5426    fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
5427        let Ok(node_range) = SourceRange::try_from(&node) else {
5428            return Ok(true);
5429        };
5430
5431        if let crate::walk::Node::SketchBlock(sketch_block) = node {
5432            if node_range.module_id() == self.target_before_mutation.module_id()
5433                && node_range.start() == self.target_before_mutation.start()
5434                // End shouldn't match since we added something.
5435                && node_range.end() >= self.target_before_mutation.end()
5436            {
5437                self.found.set(sketch_block.body.items.last().map(|item| match item {
5438                    // For declarations like `circle1 = circle(...)`, use
5439                    // the init expression range so lookup in source_range_to_object
5440                    // matches the segment source range.
5441                    ast::BodyItem::VariableDeclaration(node) => AstNodeRef::from(&node.declaration.init),
5442                    _ => AstNodeRef::from(item),
5443                }));
5444                return Ok(false);
5445            } else {
5446                // We found a different sketch block. No need to descend into
5447                // its children since sketch blocks cannot be nested.
5448                return Ok(true);
5449            }
5450        }
5451
5452        for child in node.children().iter() {
5453            if !child.visit(*self)? {
5454                return Ok(false);
5455            }
5456        }
5457
5458        Ok(true)
5459    }
5460}
5461
5462struct FindSketchBlockByNodePath {
5463    /// The Node Path of the sketch block before mutation.
5464    target_node_path: ast::NodePath,
5465    /// The ref of the sketch block's last body item after mutation. We need to
5466    /// use a [Cell] since the [crate::walk::Visitor] trait requires a shared
5467    /// reference.
5468    found: Cell<Option<AstNodeRef>>,
5469}
5470
5471impl<'a> crate::walk::Visitor<'a> for &FindSketchBlockByNodePath {
5472    type Error = crate::front::Error;
5473
5474    fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
5475        let Ok(node_path) = <Option<ast::NodePath>>::try_from(&node) else {
5476            return Ok(true);
5477        };
5478
5479        if let crate::walk::Node::SketchBlock(sketch_block) = node {
5480            if let Some(node_path) = node_path
5481                && node_path == self.target_node_path
5482            {
5483                self.found.set(sketch_block.body.items.last().map(|item| match item {
5484                    // For declarations like `circle1 = circle(...)`, use
5485                    // the init expression range so lookup in source_range_to_object
5486                    // matches the segment source range.
5487                    ast::BodyItem::VariableDeclaration(node) => AstNodeRef::from(&node.declaration.init),
5488                    _ => AstNodeRef::from(item),
5489                }));
5490
5491                return Ok(false);
5492            } else {
5493                // We found a different sketch block. No need to descend into
5494                // its children since sketch blocks cannot be nested.
5495                return Ok(true);
5496            }
5497        }
5498
5499        for child in node.children().iter() {
5500            if !child.visit(*self)? {
5501                return Ok(false);
5502            }
5503        }
5504
5505        Ok(true)
5506    }
5507}
5508
5509/// After adding an item to a sketch block, find the sketch block, and get the
5510/// source range of the added item. We assume that the added item is the last
5511/// item in the sketch block and that the sketch block's source range has grown,
5512/// but not moved from its starting offset.
5513///
5514/// TODO: Do we need to format *before* mutation in case formatting moves the
5515/// sketch block forward?
5516fn find_sketch_block_added_item(
5517    ast: &ast::Node<ast::Program>,
5518    sketch_block_before_mutation: &AstNodeRef,
5519) -> Result<AstNodeRef, KclError> {
5520    if let Some(node_path) = &sketch_block_before_mutation.node_path {
5521        let find = FindSketchBlockByNodePath {
5522            target_node_path: node_path.clone(),
5523            found: Cell::new(None),
5524        };
5525        let node = crate::walk::Node::from(ast);
5526        node.visit(&find).map_err(|err| KclError::refactor(err.msg))?;
5527        find.found.into_inner().ok_or_else(|| {
5528            KclError::refactor(format!(
5529                "Node ID after mutation not found for Node ID before mutation: {node_path:?}"
5530            ))
5531        })
5532    } else {
5533        // No NodePath. Fall back to legacy source range.
5534        let find = FindSketchBlockSourceRange {
5535            target_before_mutation: sketch_block_before_mutation.range,
5536            found: Cell::new(None),
5537        };
5538        let node = crate::walk::Node::from(ast);
5539        node.visit(&find).map_err(|err| KclError::refactor(err.msg))?;
5540        find.found.into_inner().ok_or_else(|| KclError::refactor(
5541            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?"),
5542        ))
5543    }
5544}
5545
5546fn source_from_ast(ast: &ast::Node<ast::Program>) -> String {
5547    // TODO: Don't duplicate this from lib.rs Program.
5548    ast.recast_top(&Default::default(), 0)
5549}
5550
5551pub(crate) fn to_ast_point2d(point: &Point2d<Expr>) -> anyhow::Result<ast::Expr> {
5552    Ok(ast::Expr::ArrayExpression(Box::new(ast::Node {
5553        inner: ast::ArrayExpression {
5554            elements: vec![to_source_expr(&point.x)?, to_source_expr(&point.y)?],
5555            non_code_meta: Default::default(),
5556            digest: None,
5557        },
5558        start: Default::default(),
5559        end: Default::default(),
5560        module_id: Default::default(),
5561        node_path: None,
5562        outer_attrs: Default::default(),
5563        pre_comments: Default::default(),
5564        comment_start: Default::default(),
5565    })))
5566}
5567
5568fn to_ast_point2d_number(point: &Point2d<Number>) -> anyhow::Result<ast::Expr> {
5569    Ok(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
5570        ast::ArrayExpression {
5571            elements: vec![
5572                ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(to_source_number(
5573                    point.x,
5574                )?)))),
5575                ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(to_source_number(
5576                    point.y,
5577                )?)))),
5578            ],
5579            non_code_meta: Default::default(),
5580            digest: None,
5581        },
5582    ))))
5583}
5584
5585fn to_source_expr(expr: &Expr) -> anyhow::Result<ast::Expr> {
5586    match expr {
5587        Expr::Number(number) => Ok(ast::Expr::Literal(Box::new(ast::Node {
5588            inner: ast::Literal::from(to_source_number(*number)?),
5589            start: Default::default(),
5590            end: Default::default(),
5591            module_id: Default::default(),
5592            node_path: None,
5593            outer_attrs: Default::default(),
5594            pre_comments: Default::default(),
5595            comment_start: Default::default(),
5596        }))),
5597        Expr::Var(number) => Ok(ast::Expr::SketchVar(Box::new(ast::Node {
5598            inner: ast::SketchVar {
5599                initial: Some(Box::new(ast::Node {
5600                    inner: to_source_number(*number)?,
5601                    start: Default::default(),
5602                    end: Default::default(),
5603                    module_id: Default::default(),
5604                    node_path: None,
5605                    outer_attrs: Default::default(),
5606                    pre_comments: Default::default(),
5607                    comment_start: Default::default(),
5608                })),
5609                digest: None,
5610            },
5611            start: Default::default(),
5612            end: Default::default(),
5613            module_id: Default::default(),
5614            node_path: None,
5615            outer_attrs: Default::default(),
5616            pre_comments: Default::default(),
5617            comment_start: Default::default(),
5618        }))),
5619        Expr::Variable(variable) => Ok(ast_name_expr(variable.clone())),
5620    }
5621}
5622
5623fn to_source_number(number: Number) -> anyhow::Result<ast::NumericLiteral> {
5624    Ok(ast::NumericLiteral {
5625        value: number.value,
5626        suffix: number.units,
5627        raw: format_number_literal(number.value, number.units, None)?,
5628        digest: None,
5629    })
5630}
5631
5632pub(crate) fn ast_name_expr(name: String) -> ast::Expr {
5633    ast::Expr::Name(Box::new(ast_name(name)))
5634}
5635
5636fn ast_name(name: String) -> ast::Node<ast::Name> {
5637    ast::Node {
5638        inner: ast::Name {
5639            name: ast::Node {
5640                inner: ast::Identifier { name, digest: None },
5641                start: Default::default(),
5642                end: Default::default(),
5643                module_id: Default::default(),
5644                node_path: None,
5645                outer_attrs: Default::default(),
5646                pre_comments: Default::default(),
5647                comment_start: Default::default(),
5648            },
5649            path: Vec::new(),
5650            abs_path: false,
5651            digest: None,
5652        },
5653        start: Default::default(),
5654        end: Default::default(),
5655        module_id: Default::default(),
5656        node_path: None,
5657        outer_attrs: Default::default(),
5658        pre_comments: Default::default(),
5659        comment_start: Default::default(),
5660    }
5661}
5662
5663pub(crate) fn ast_sketch2_name(name: &str) -> ast::Name {
5664    ast::Name {
5665        name: ast::Node {
5666            inner: ast::Identifier {
5667                name: name.to_owned(),
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        path: Default::default(),
5679        abs_path: false,
5680        digest: None,
5681    }
5682}
5683
5684// Shared AST creation helpers used by both frontend and transpiler to ensure consistency.
5685
5686/// Create an AST node for coincident([expr1, expr2, ...])
5687pub(crate) fn create_coincident_ast(exprs: impl IntoIterator<Item = ast::Expr>) -> ast::Expr {
5688    let elements = exprs.into_iter().collect::<Vec<_>>();
5689    debug_assert!(elements.len() >= 2, "Coincident AST should have at least 2 inputs");
5690
5691    // Create array [expr1, expr2, ...]
5692    let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
5693        elements,
5694        digest: None,
5695        non_code_meta: Default::default(),
5696    })));
5697
5698    // Create coincident([...])
5699    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5700        callee: ast::Node::no_src(ast_sketch2_name(COINCIDENT_FN)),
5701        unlabeled: Some(array_expr),
5702        arguments: Default::default(),
5703        digest: None,
5704        non_code_meta: Default::default(),
5705    })))
5706}
5707
5708/// Create an AST node for line(start = [...], end = [...])
5709pub(crate) fn create_line_ast(start_ast: ast::Expr, end_ast: ast::Expr) -> ast::Expr {
5710    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5711        callee: ast::Node::no_src(ast_sketch2_name(LINE_FN)),
5712        unlabeled: None,
5713        arguments: vec![
5714            ast::LabeledArg {
5715                label: Some(ast::Identifier::new(LINE_START_PARAM)),
5716                arg: start_ast,
5717            },
5718            ast::LabeledArg {
5719                label: Some(ast::Identifier::new(LINE_END_PARAM)),
5720                arg: end_ast,
5721            },
5722        ],
5723        digest: None,
5724        non_code_meta: Default::default(),
5725    })))
5726}
5727
5728/// Create an AST node for arc(start = [...], end = [...], center = [...])
5729pub(crate) fn create_arc_ast(start_ast: ast::Expr, end_ast: ast::Expr, center_ast: ast::Expr) -> ast::Expr {
5730    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5731        callee: ast::Node::no_src(ast_sketch2_name(ARC_FN)),
5732        unlabeled: None,
5733        arguments: vec![
5734            ast::LabeledArg {
5735                label: Some(ast::Identifier::new(ARC_START_PARAM)),
5736                arg: start_ast,
5737            },
5738            ast::LabeledArg {
5739                label: Some(ast::Identifier::new(ARC_END_PARAM)),
5740                arg: end_ast,
5741            },
5742            ast::LabeledArg {
5743                label: Some(ast::Identifier::new(ARC_CENTER_PARAM)),
5744                arg: center_ast,
5745            },
5746        ],
5747        digest: None,
5748        non_code_meta: Default::default(),
5749    })))
5750}
5751
5752/// Create an AST node for circle(start = [...], center = [...])
5753pub(crate) fn create_circle_ast(start_ast: ast::Expr, center_ast: ast::Expr) -> ast::Expr {
5754    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5755        callee: ast::Node::no_src(ast_sketch2_name(CIRCLE_FN)),
5756        unlabeled: None,
5757        arguments: vec![
5758            ast::LabeledArg {
5759                label: Some(ast::Identifier::new(CIRCLE_START_PARAM)),
5760                arg: start_ast,
5761            },
5762            ast::LabeledArg {
5763                label: Some(ast::Identifier::new(CIRCLE_CENTER_PARAM)),
5764                arg: center_ast,
5765            },
5766        ],
5767        digest: None,
5768        non_code_meta: Default::default(),
5769    })))
5770}
5771
5772/// Create an AST node for horizontal(line)
5773pub(crate) fn create_horizontal_ast(line_expr: ast::Expr) -> ast::Expr {
5774    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5775        callee: ast::Node::no_src(ast_sketch2_name(HORIZONTAL_FN)),
5776        unlabeled: Some(line_expr),
5777        arguments: Default::default(),
5778        digest: None,
5779        non_code_meta: Default::default(),
5780    })))
5781}
5782
5783/// Create an AST node for vertical(line)
5784pub(crate) fn create_vertical_ast(line_expr: ast::Expr) -> ast::Expr {
5785    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5786        callee: ast::Node::no_src(ast_sketch2_name(VERTICAL_FN)),
5787        unlabeled: Some(line_expr),
5788        arguments: Default::default(),
5789        digest: None,
5790        non_code_meta: Default::default(),
5791    })))
5792}
5793
5794/// Create a member expression like object.property (e.g., line1.end)
5795pub(crate) fn create_member_expression(object_expr: ast::Expr, property: &str) -> ast::Expr {
5796    ast::Expr::MemberExpression(Box::new(ast::Node::no_src(ast::MemberExpression {
5797        object: object_expr,
5798        property: ast::Expr::Name(Box::new(ast::Node::no_src(ast::Name {
5799            name: ast::Node::no_src(ast::Identifier {
5800                name: property.to_string(),
5801                digest: None,
5802            }),
5803            path: Vec::new(),
5804            abs_path: false,
5805            digest: None,
5806        }))),
5807        computed: false,
5808        digest: None,
5809    })))
5810}
5811
5812/// Create an AST node for `fixed([point, [x, y]])`.
5813fn create_fixed_point_constraint_ast(point_expr: ast::Expr, position: Point2d<Number>) -> anyhow::Result<ast::Expr> {
5814    // Create [x, y] array literal.
5815    let x_literal = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(to_source_number(
5816        position.x,
5817    )?))));
5818    let y_literal = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(to_source_number(
5819        position.y,
5820    )?))));
5821    let point_array = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
5822        elements: vec![x_literal, y_literal],
5823        digest: None,
5824        non_code_meta: Default::default(),
5825    })));
5826
5827    // Create [point, [x, y]] outer array.
5828    let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
5829        elements: vec![point_expr, point_array],
5830        digest: None,
5831        non_code_meta: Default::default(),
5832    })));
5833
5834    // Create fixed([...])
5835    Ok(ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(
5836        ast::CallExpressionKw {
5837            callee: ast::Node::no_src(ast_sketch2_name(FIXED_FN)),
5838            unlabeled: Some(array_expr),
5839            arguments: Default::default(),
5840            digest: None,
5841            non_code_meta: Default::default(),
5842        },
5843    ))))
5844}
5845
5846/// Create an AST node for equalLength([line1, line2, ...])
5847pub(crate) fn create_equal_length_ast(line_exprs: Vec<ast::Expr>) -> ast::Expr {
5848    let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
5849        elements: line_exprs,
5850        digest: None,
5851        non_code_meta: Default::default(),
5852    })));
5853
5854    // Create equalLength([...])
5855    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5856        callee: ast::Node::no_src(ast_sketch2_name(EQUAL_LENGTH_FN)),
5857        unlabeled: Some(array_expr),
5858        arguments: Default::default(),
5859        digest: None,
5860        non_code_meta: Default::default(),
5861    })))
5862}
5863
5864/// Create an AST node for equalRadius([seg1, seg2, ...])
5865pub(crate) fn create_equal_radius_ast(segment_exprs: Vec<ast::Expr>) -> ast::Expr {
5866    let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
5867        elements: segment_exprs,
5868        digest: None,
5869        non_code_meta: Default::default(),
5870    })));
5871
5872    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5873        callee: ast::Node::no_src(ast_sketch2_name(EQUAL_RADIUS_FN)),
5874        unlabeled: Some(array_expr),
5875        arguments: Default::default(),
5876        digest: None,
5877        non_code_meta: Default::default(),
5878    })))
5879}
5880
5881/// Create an AST node for tangent([seg1, seg2])
5882pub(crate) fn create_tangent_ast(seg1_expr: ast::Expr, seg2_expr: ast::Expr) -> ast::Expr {
5883    let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
5884        elements: vec![seg1_expr, seg2_expr],
5885        digest: None,
5886        non_code_meta: Default::default(),
5887    })));
5888
5889    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5890        callee: ast::Node::no_src(ast_sketch2_name(TANGENT_FN)),
5891        unlabeled: Some(array_expr),
5892        arguments: Default::default(),
5893        digest: None,
5894        non_code_meta: Default::default(),
5895    })))
5896}
5897
5898/// Create an AST node for symmetric([input1, input2], axis = line)
5899pub(crate) fn create_symmetric_ast(input_exprs: Vec<ast::Expr>, axis_expr: ast::Expr) -> ast::Expr {
5900    let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
5901        elements: input_exprs,
5902        digest: None,
5903        non_code_meta: Default::default(),
5904    })));
5905    let arguments = vec![ast::LabeledArg {
5906        label: Some(ast::Identifier::new(SYMMETRIC_AXIS_PARAM)),
5907        arg: axis_expr,
5908    }];
5909
5910    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5911        callee: ast::Node::no_src(ast_sketch2_name(SYMMETRIC_FN)),
5912        unlabeled: Some(array_expr),
5913        arguments,
5914        digest: None,
5915        non_code_meta: Default::default(),
5916    })))
5917}
5918
5919/// Create an AST node for midpoint(segment, point = point)
5920pub(crate) fn create_midpoint_ast(segment_expr: ast::Expr, point_expr: ast::Expr) -> ast::Expr {
5921    let arguments = vec![ast::LabeledArg {
5922        label: Some(ast::Identifier::new(MIDPOINT_POINT_PARAM)),
5923        arg: point_expr,
5924    }];
5925
5926    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5927        callee: ast::Node::no_src(ast_sketch2_name(MIDPOINT_FN)),
5928        unlabeled: Some(segment_expr),
5929        arguments,
5930        digest: None,
5931        non_code_meta: Default::default(),
5932    })))
5933}
5934
5935#[cfg(test)]
5936mod tests {
5937    use super::*;
5938    use crate::engine::PlaneName;
5939    use crate::execution::cache::SketchModeState;
5940    use crate::execution::cache::clear_mem_cache;
5941    use crate::execution::cache::read_old_memory;
5942    use crate::execution::cache::write_old_memory;
5943    use crate::front::Distance;
5944    use crate::front::Fixed;
5945    use crate::front::FixedPoint;
5946    use crate::front::Midpoint;
5947    use crate::front::Object;
5948    use crate::front::Plane;
5949    use crate::front::Sketch;
5950    use crate::front::Tangent;
5951    use crate::frontend::sketch::Vertical;
5952    use crate::pretty::NumericSuffix;
5953
5954    fn find_first_sketch_object(scene_graph: &SceneGraph) -> Option<&Object> {
5955        for object in &scene_graph.objects {
5956            if let ObjectKind::Sketch(_) = &object.kind {
5957                return Some(object);
5958            }
5959        }
5960        None
5961    }
5962
5963    fn find_first_face_object(scene_graph: &SceneGraph) -> Option<&Object> {
5964        for object in &scene_graph.objects {
5965            if let ObjectKind::Face(_) = &object.kind {
5966                return Some(object);
5967            }
5968        }
5969        None
5970    }
5971
5972    fn find_first_wall_object_id(scene_graph: &SceneGraph) -> Option<ObjectId> {
5973        for object in &scene_graph.objects {
5974            if matches!(&object.kind, ObjectKind::Wall(_)) {
5975                return Some(object.id);
5976            }
5977        }
5978        None
5979    }
5980
5981    #[test]
5982    fn test_region_name_from_sweep_variable_supports_sweep_kinds() {
5983        let source = "\
5984region001 = region(point = [0.1, 0.1], sketch = s)
5985extrude001 = extrude(region001, length = 5)
5986revolve001 = revolve(region001, axis = Y)
5987sweep001 = sweep(region001, path = path001)
5988loft001 = loft(region001)
5989not_sweep001 = shell(extrude001, faces = [], thickness = 1)
5990";
5991
5992        let program = Program::parse(source).unwrap().0.unwrap();
5993
5994        assert_eq!(
5995            region_name_from_sweep_variable(&program.ast, "extrude001"),
5996            Some("region001".to_owned())
5997        );
5998        assert_eq!(
5999            region_name_from_sweep_variable(&program.ast, "revolve001"),
6000            Some("region001".to_owned())
6001        );
6002        assert_eq!(
6003            region_name_from_sweep_variable(&program.ast, "sweep001"),
6004            Some("region001".to_owned())
6005        );
6006        assert_eq!(
6007            region_name_from_sweep_variable(&program.ast, "loft001"),
6008            Some("region001".to_owned())
6009        );
6010        assert_eq!(region_name_from_sweep_variable(&program.ast, "not_sweep001"), None);
6011    }
6012
6013    #[track_caller]
6014    fn expect_sketch(object: &Object) -> &Sketch {
6015        if let ObjectKind::Sketch(sketch) = &object.kind {
6016            sketch
6017        } else {
6018            panic!("Object is not a sketch: {:?}", object);
6019        }
6020    }
6021
6022    fn point_position(scene_graph: &SceneGraph, point_id: ObjectId) -> Point2d<Number> {
6023        let point_object = scene_graph.objects.get(point_id.0).unwrap();
6024        let ObjectKind::Segment {
6025            segment: Segment::Point(point),
6026        } = &point_object.kind
6027        else {
6028            panic!("Object is not a point segment: {point_object:?}");
6029        };
6030        point.position.clone()
6031    }
6032
6033    fn assert_point_position_close(actual: Point2d<Number>, expected: Point2d<Number>) {
6034        assert!((actual.x.value - expected.x.value).abs() < 1e-6);
6035        assert!((actual.y.value - expected.y.value).abs() < 1e-6);
6036    }
6037
6038    fn make_line_ctor(start_x: f64, start_y: f64, end_x: f64, end_y: f64, units: NumericSuffix) -> LineCtor {
6039        LineCtor {
6040            start: Point2d {
6041                x: Expr::Number(Number { value: start_x, units }),
6042                y: Expr::Number(Number { value: start_y, units }),
6043            },
6044            end: Point2d {
6045                x: Expr::Number(Number { value: end_x, units }),
6046                y: Expr::Number(Number { value: end_y, units }),
6047            },
6048            construction: None,
6049        }
6050    }
6051
6052    async fn create_sketch_with_single_line(
6053        frontend: &mut FrontendState,
6054        ctx: &ExecutorContext,
6055        mock_ctx: &ExecutorContext,
6056        version: Version,
6057    ) -> (ObjectId, ObjectId, SourceDelta, SceneGraphDelta) {
6058        frontend.program = Program::empty();
6059
6060        let sketch_args = SketchCtor {
6061            on: Plane::Default(PlaneName::Xy),
6062        };
6063        let (_src_delta, _scene_delta, sketch_id) = frontend
6064            .new_sketch(ctx, ProjectId(0), FileId(0), version, sketch_args)
6065            .await
6066            .unwrap();
6067
6068        let segment = SegmentCtor::Line(make_line_ctor(0.0, 0.0, 10.0, 10.0, NumericSuffix::Mm));
6069        let (source_delta, scene_graph_delta) = frontend
6070            .add_segment(mock_ctx, version, sketch_id, segment, None)
6071            .await
6072            .unwrap();
6073        let line_id = *scene_graph_delta
6074            .new_objects
6075            .last()
6076            .expect("Expected line object id to be created");
6077
6078        (sketch_id, line_id, source_delta, scene_graph_delta)
6079    }
6080
6081    #[tokio::test(flavor = "multi_thread")]
6082    async fn test_sketch_checkpoint_round_trip_restores_state() {
6083        let mut frontend = FrontendState::new();
6084        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6085        let mock_ctx = ExecutorContext::new_mock(None).await;
6086        let version = Version(0);
6087
6088        let (sketch_id, line_id, source_delta, scene_graph_delta) =
6089            create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
6090
6091        let expected_source = source_delta.text.clone();
6092        let expected_scene_graph = frontend.scene_graph.clone();
6093        let expected_exec_outcome = scene_graph_delta.exec_outcome.clone();
6094        let expected_point_freedom_cache = frontend.point_freedom_cache.clone();
6095
6096        let checkpoint_id = frontend
6097            .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
6098            .await
6099            .unwrap();
6100
6101        let edited_segments = vec![ExistingSegmentCtor {
6102            id: line_id,
6103            ctor: SegmentCtor::Line(make_line_ctor(1.0, 2.0, 13.0, 14.0, NumericSuffix::Mm)),
6104        }];
6105        let (edited_source, _edited_scene) = frontend
6106            .edit_segments(&mock_ctx, version, sketch_id, edited_segments)
6107            .await
6108            .unwrap();
6109        assert_ne!(edited_source.text, expected_source);
6110
6111        let restored = frontend.restore_sketch_checkpoint(checkpoint_id).await.unwrap();
6112
6113        assert_eq!(restored.source_delta.text, expected_source);
6114        assert_eq!(restored.scene_graph_delta.new_graph, expected_scene_graph);
6115        assert!(restored.scene_graph_delta.invalidates_ids);
6116        assert_eq!(restored.scene_graph_delta.exec_outcome, expected_exec_outcome);
6117        assert_eq!(frontend.scene_graph, expected_scene_graph);
6118        assert_eq!(frontend.point_freedom_cache, expected_point_freedom_cache);
6119
6120        ctx.close().await;
6121        mock_ctx.close().await;
6122    }
6123
6124    #[tokio::test(flavor = "multi_thread")]
6125    async fn test_sketch_checkpoints_prune_oldest_entries() {
6126        let mut frontend = FrontendState::new();
6127        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6128        let mock_ctx = ExecutorContext::new_mock(None).await;
6129        let version = Version(0);
6130
6131        let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
6132            create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
6133
6134        let mut checkpoint_ids = Vec::new();
6135        for _ in 0..(MAX_SKETCH_CHECKPOINTS + 3) {
6136            checkpoint_ids.push(
6137                frontend
6138                    .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
6139                    .await
6140                    .unwrap(),
6141            );
6142        }
6143
6144        assert_eq!(frontend.sketch_checkpoints.len(), MAX_SKETCH_CHECKPOINTS);
6145        assert!(checkpoint_ids.windows(2).all(|ids| ids[0] < ids[1]));
6146
6147        let oldest_retained = checkpoint_ids[3];
6148        assert_eq!(
6149            frontend.sketch_checkpoints.front().map(|checkpoint| checkpoint.id),
6150            Some(oldest_retained)
6151        );
6152
6153        let evicted_restore = frontend.restore_sketch_checkpoint(checkpoint_ids[0]).await;
6154        assert!(evicted_restore.is_err());
6155        assert!(evicted_restore.unwrap_err().msg.contains("Sketch checkpoint not found"));
6156
6157        frontend
6158            .restore_sketch_checkpoint(*checkpoint_ids.last().unwrap())
6159            .await
6160            .unwrap();
6161
6162        ctx.close().await;
6163        mock_ctx.close().await;
6164    }
6165
6166    #[tokio::test(flavor = "multi_thread")]
6167    async fn test_restore_sketch_checkpoint_missing_id_returns_error() {
6168        let mut frontend = FrontendState::new();
6169        let missing_checkpoint = SketchCheckpointId::new(999);
6170
6171        let err = frontend
6172            .restore_sketch_checkpoint(missing_checkpoint)
6173            .await
6174            .expect_err("Expected restore to fail for missing checkpoint");
6175
6176        assert!(err.msg.contains("Sketch checkpoint not found"));
6177    }
6178
6179    #[tokio::test(flavor = "multi_thread")]
6180    async fn test_clear_sketch_checkpoints_removes_all_restore_points() {
6181        let mut frontend = FrontendState::new();
6182        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6183        let mock_ctx = ExecutorContext::new_mock(None).await;
6184        let version = Version(0);
6185
6186        let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
6187            create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
6188
6189        let checkpoint_a = frontend
6190            .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
6191            .await
6192            .unwrap();
6193        let checkpoint_b = frontend
6194            .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
6195            .await
6196            .unwrap();
6197        assert_eq!(frontend.sketch_checkpoints.len(), 2);
6198
6199        frontend.clear_sketch_checkpoints();
6200        assert!(frontend.sketch_checkpoints.is_empty());
6201        frontend.restore_sketch_checkpoint(checkpoint_a).await.unwrap_err();
6202        frontend.restore_sketch_checkpoint(checkpoint_b).await.unwrap_err();
6203
6204        ctx.close().await;
6205        mock_ctx.close().await;
6206    }
6207
6208    #[tokio::test(flavor = "multi_thread")]
6209    async fn test_hack_set_program_keeps_old_checkpoints_and_adds_fresh_baseline() {
6210        let mut frontend = FrontendState::new();
6211        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6212        let mock_ctx = ExecutorContext::new_mock(None).await;
6213        let version = Version(0);
6214
6215        let (_sketch_id, _line_id, source_delta, scene_graph_delta) =
6216            create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
6217        let old_source = source_delta.text.clone();
6218        let old_checkpoint = frontend
6219            .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
6220            .await
6221            .unwrap();
6222        let initial_checkpoint_count = frontend.sketch_checkpoints.len();
6223
6224        let new_program = Program::parse("sketch(on = XY) {\n  point(at = [1mm, 2mm])\n}\n")
6225            .unwrap()
6226            .0
6227            .unwrap();
6228
6229        let result = frontend.hack_set_program(&ctx, new_program).await.unwrap();
6230        let SetProgramOutcome::Success {
6231            checkpoint_id: Some(new_checkpoint),
6232            ..
6233        } = result
6234        else {
6235            panic!("Expected Success with a fresh checkpoint baseline");
6236        };
6237
6238        assert_eq!(frontend.sketch_checkpoints.len(), initial_checkpoint_count + 1);
6239
6240        let old_restore = frontend.restore_sketch_checkpoint(old_checkpoint).await.unwrap();
6241        assert_eq!(old_restore.source_delta.text, old_source);
6242
6243        let new_restore = frontend.restore_sketch_checkpoint(new_checkpoint).await.unwrap();
6244        assert!(new_restore.source_delta.text.contains("point(at = [1mm, 2mm])"));
6245
6246        ctx.close().await;
6247        mock_ctx.close().await;
6248    }
6249
6250    #[tokio::test(flavor = "multi_thread")]
6251    async fn test_hack_set_program_exec_failure_does_not_add_checkpoint() {
6252        let mut frontend = FrontendState::new();
6253        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6254        let mock_ctx = ExecutorContext::new_mock(None).await;
6255        let version = Version(0);
6256
6257        let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
6258            create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
6259        let old_checkpoint = frontend
6260            .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
6261            .await
6262            .unwrap();
6263        let checkpoint_count_before = frontend.sketch_checkpoints.len();
6264
6265        let failing_program = Program::parse(
6266            "sketch(on = XY) {\n  line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])\n}\n\nbad = missing_name\n",
6267        )
6268        .unwrap()
6269        .0
6270        .unwrap();
6271
6272        let result = frontend.hack_set_program(&ctx, failing_program).await.unwrap();
6273        assert!(matches!(result, SetProgramOutcome::ExecFailure { .. }));
6274        assert_eq!(frontend.sketch_checkpoints.len(), checkpoint_count_before);
6275        frontend.restore_sketch_checkpoint(old_checkpoint).await.unwrap();
6276
6277        ctx.close().await;
6278        mock_ctx.close().await;
6279    }
6280
6281    #[tokio::test(flavor = "multi_thread")]
6282    async fn test_restore_sketch_checkpoint_restores_and_clears_mock_memory() {
6283        let mut frontend = FrontendState::new();
6284        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6285
6286        let program = Program::parse(
6287            "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",
6288        )
6289        .unwrap()
6290        .0
6291        .unwrap();
6292        let set_program_outcome = frontend.hack_set_program(&ctx, program).await.unwrap();
6293        let SetProgramOutcome::Success { exec_outcome, .. } = set_program_outcome else {
6294            panic!("Expected successful baseline program execution");
6295        };
6296
6297        clear_mem_cache().await;
6298        assert!(read_old_memory().await.is_none());
6299
6300        let checkpoint_without_mock_memory = frontend
6301            .create_sketch_checkpoint((*exec_outcome).clone())
6302            .await
6303            .unwrap();
6304
6305        write_old_memory(SketchModeState::new_for_tests()).await;
6306        assert!(read_old_memory().await.is_some());
6307
6308        let checkpoint_with_mock_memory = frontend
6309            .create_sketch_checkpoint((*exec_outcome).clone())
6310            .await
6311            .unwrap();
6312
6313        clear_mem_cache().await;
6314        assert!(read_old_memory().await.is_none());
6315
6316        frontend
6317            .restore_sketch_checkpoint(checkpoint_with_mock_memory)
6318            .await
6319            .unwrap();
6320        assert!(read_old_memory().await.is_some());
6321
6322        frontend
6323            .restore_sketch_checkpoint(checkpoint_without_mock_memory)
6324            .await
6325            .unwrap();
6326        assert!(read_old_memory().await.is_none());
6327
6328        ctx.close().await;
6329    }
6330
6331    #[tokio::test(flavor = "multi_thread")]
6332    async fn test_hack_set_program_exec_error_still_allows_edit_sketch() {
6333        let source = "\
6334sketch(on = XY) {
6335  line1 = line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])
6336}
6337
6338bad = missing_name
6339";
6340        let program = Program::parse(source).unwrap().0.unwrap();
6341
6342        let mut frontend = FrontendState::new();
6343
6344        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6345        let mock_ctx = ExecutorContext::new_mock(None).await;
6346        let version = Version(0);
6347        let project_id = ProjectId(0);
6348        let file_id = FileId(0);
6349
6350        let SetProgramOutcome::ExecFailure { .. } = frontend.hack_set_program(&ctx, program).await.unwrap() else {
6351            panic!("Expected ExecFailure from hack_set_program due to syntax error in program");
6352        };
6353
6354        let sketch_id = frontend
6355            .scene_graph
6356            .objects
6357            .iter()
6358            .find_map(|obj| matches!(obj.kind, ObjectKind::Sketch(_)).then_some(obj.id))
6359            .expect("Expected sketch object from errored hack_set_program");
6360
6361        frontend
6362            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
6363            .await
6364            .unwrap();
6365
6366        ctx.close().await;
6367        mock_ctx.close().await;
6368    }
6369
6370    #[tokio::test(flavor = "multi_thread")]
6371    async fn test_new_sketch_add_point_edit_point() {
6372        let program = Program::empty();
6373
6374        let mut frontend = FrontendState::new();
6375        frontend.program = program;
6376
6377        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6378        let mock_ctx = ExecutorContext::new_mock(None).await;
6379        let version = Version(0);
6380
6381        let sketch_args = SketchCtor {
6382            on: Plane::Default(PlaneName::Xy),
6383        };
6384        let (_src_delta, scene_delta, sketch_id) = frontend
6385            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
6386            .await
6387            .unwrap();
6388        assert_eq!(sketch_id, ObjectId(1));
6389        assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
6390        let sketch_object = &scene_delta.new_graph.objects[1];
6391        assert_eq!(sketch_object.id, ObjectId(1));
6392        assert_eq!(
6393            sketch_object.kind,
6394            ObjectKind::Sketch(Sketch {
6395                args: SketchCtor {
6396                    on: Plane::Default(PlaneName::Xy)
6397                },
6398                plane: ObjectId(0),
6399                segments: vec![],
6400                constraints: vec![],
6401            })
6402        );
6403        assert_eq!(scene_delta.new_graph.objects.len(), 2);
6404
6405        let point_ctor = PointCtor {
6406            position: Point2d {
6407                x: Expr::Number(Number {
6408                    value: 1.0,
6409                    units: NumericSuffix::Inch,
6410                }),
6411                y: Expr::Number(Number {
6412                    value: 2.0,
6413                    units: NumericSuffix::Inch,
6414                }),
6415            },
6416        };
6417        let segment = SegmentCtor::Point(point_ctor);
6418        let (src_delta, scene_delta) = frontend
6419            .add_segment(&mock_ctx, version, sketch_id, segment, None)
6420            .await
6421            .unwrap();
6422        assert_eq!(
6423            src_delta.text.as_str(),
6424            "sketch001 = sketch(on = XY) {
6425  point(at = [1in, 2in])
6426}
6427"
6428        );
6429        assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
6430        assert_eq!(scene_delta.new_graph.objects.len(), 3);
6431        for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
6432            assert_eq!(scene_object.id.0, i);
6433        }
6434
6435        let point_id = *scene_delta.new_objects.last().unwrap();
6436
6437        let point_ctor = PointCtor {
6438            position: Point2d {
6439                x: Expr::Number(Number {
6440                    value: 3.0,
6441                    units: NumericSuffix::Inch,
6442                }),
6443                y: Expr::Number(Number {
6444                    value: 4.0,
6445                    units: NumericSuffix::Inch,
6446                }),
6447            },
6448        };
6449        let segments = vec![ExistingSegmentCtor {
6450            id: point_id,
6451            ctor: SegmentCtor::Point(point_ctor),
6452        }];
6453        let (src_delta, scene_delta) = frontend
6454            .edit_segments(&mock_ctx, version, sketch_id, segments)
6455            .await
6456            .unwrap();
6457        assert_eq!(
6458            src_delta.text.as_str(),
6459            "sketch001 = sketch(on = XY) {
6460  point(at = [3in, 4in])
6461}
6462"
6463        );
6464        assert_eq!(scene_delta.new_objects, vec![]);
6465        assert_eq!(scene_delta.new_graph.objects.len(), 3);
6466
6467        ctx.close().await;
6468        mock_ctx.close().await;
6469    }
6470
6471    #[tokio::test(flavor = "multi_thread")]
6472    async fn test_new_sketch_add_line_edit_line() {
6473        let program = Program::empty();
6474
6475        let mut frontend = FrontendState::new();
6476        frontend.program = program;
6477
6478        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6479        let mock_ctx = ExecutorContext::new_mock(None).await;
6480        let version = Version(0);
6481
6482        let sketch_args = SketchCtor {
6483            on: Plane::Default(PlaneName::Xy),
6484        };
6485        let (_src_delta, scene_delta, sketch_id) = frontend
6486            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
6487            .await
6488            .unwrap();
6489        assert_eq!(sketch_id, ObjectId(1));
6490        assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
6491        let sketch_object = &scene_delta.new_graph.objects[1];
6492        assert_eq!(sketch_object.id, ObjectId(1));
6493        assert_eq!(
6494            sketch_object.kind,
6495            ObjectKind::Sketch(Sketch {
6496                args: SketchCtor {
6497                    on: Plane::Default(PlaneName::Xy)
6498                },
6499                plane: ObjectId(0),
6500                segments: vec![],
6501                constraints: vec![],
6502            })
6503        );
6504        assert_eq!(scene_delta.new_graph.objects.len(), 2);
6505
6506        let line_ctor = LineCtor {
6507            start: Point2d {
6508                x: Expr::Number(Number {
6509                    value: 0.0,
6510                    units: NumericSuffix::Mm,
6511                }),
6512                y: Expr::Number(Number {
6513                    value: 0.0,
6514                    units: NumericSuffix::Mm,
6515                }),
6516            },
6517            end: Point2d {
6518                x: Expr::Number(Number {
6519                    value: 10.0,
6520                    units: NumericSuffix::Mm,
6521                }),
6522                y: Expr::Number(Number {
6523                    value: 10.0,
6524                    units: NumericSuffix::Mm,
6525                }),
6526            },
6527            construction: None,
6528        };
6529        let segment = SegmentCtor::Line(line_ctor);
6530        let (src_delta, scene_delta) = frontend
6531            .add_segment(&mock_ctx, version, sketch_id, segment, None)
6532            .await
6533            .unwrap();
6534        assert_eq!(
6535            src_delta.text.as_str(),
6536            "sketch001 = sketch(on = XY) {
6537  line(start = [0mm, 0mm], end = [10mm, 10mm])
6538}
6539"
6540        );
6541        assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
6542        assert_eq!(scene_delta.new_graph.objects.len(), 5);
6543        for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
6544            assert_eq!(scene_object.id.0, i);
6545        }
6546
6547        // The new objects are the end points and then the line.
6548        let line = *scene_delta.new_objects.last().unwrap();
6549
6550        let line_ctor = LineCtor {
6551            start: Point2d {
6552                x: Expr::Number(Number {
6553                    value: 1.0,
6554                    units: NumericSuffix::Mm,
6555                }),
6556                y: Expr::Number(Number {
6557                    value: 2.0,
6558                    units: NumericSuffix::Mm,
6559                }),
6560            },
6561            end: Point2d {
6562                x: Expr::Number(Number {
6563                    value: 13.0,
6564                    units: NumericSuffix::Mm,
6565                }),
6566                y: Expr::Number(Number {
6567                    value: 14.0,
6568                    units: NumericSuffix::Mm,
6569                }),
6570            },
6571            construction: None,
6572        };
6573        let segments = vec![ExistingSegmentCtor {
6574            id: line,
6575            ctor: SegmentCtor::Line(line_ctor),
6576        }];
6577        let (src_delta, scene_delta) = frontend
6578            .edit_segments(&mock_ctx, version, sketch_id, segments)
6579            .await
6580            .unwrap();
6581        assert_eq!(
6582            src_delta.text.as_str(),
6583            "sketch001 = sketch(on = XY) {
6584  line(start = [1mm, 2mm], end = [13mm, 14mm])
6585}
6586"
6587        );
6588        assert_eq!(scene_delta.new_objects, vec![]);
6589        assert_eq!(scene_delta.new_graph.objects.len(), 5);
6590
6591        ctx.close().await;
6592        mock_ctx.close().await;
6593    }
6594
6595    #[tokio::test(flavor = "multi_thread")]
6596    async fn test_new_sketch_add_arc_edit_arc() {
6597        let program = Program::empty();
6598
6599        let mut frontend = FrontendState::new();
6600        frontend.program = program;
6601
6602        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6603        let mock_ctx = ExecutorContext::new_mock(None).await;
6604        let version = Version(0);
6605
6606        let sketch_args = SketchCtor {
6607            on: Plane::Default(PlaneName::Xy),
6608        };
6609        let (_src_delta, scene_delta, sketch_id) = frontend
6610            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
6611            .await
6612            .unwrap();
6613        assert_eq!(sketch_id, ObjectId(1));
6614        assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
6615        let sketch_object = &scene_delta.new_graph.objects[1];
6616        assert_eq!(sketch_object.id, ObjectId(1));
6617        assert_eq!(
6618            sketch_object.kind,
6619            ObjectKind::Sketch(Sketch {
6620                args: SketchCtor {
6621                    on: Plane::Default(PlaneName::Xy),
6622                },
6623                plane: ObjectId(0),
6624                segments: vec![],
6625                constraints: vec![],
6626            })
6627        );
6628        assert_eq!(scene_delta.new_graph.objects.len(), 2);
6629
6630        let arc_ctor = ArcCtor {
6631            start: Point2d {
6632                x: Expr::Var(Number {
6633                    value: 0.0,
6634                    units: NumericSuffix::Mm,
6635                }),
6636                y: Expr::Var(Number {
6637                    value: 0.0,
6638                    units: NumericSuffix::Mm,
6639                }),
6640            },
6641            end: Point2d {
6642                x: Expr::Var(Number {
6643                    value: 10.0,
6644                    units: NumericSuffix::Mm,
6645                }),
6646                y: Expr::Var(Number {
6647                    value: 10.0,
6648                    units: NumericSuffix::Mm,
6649                }),
6650            },
6651            center: Point2d {
6652                x: Expr::Var(Number {
6653                    value: 10.0,
6654                    units: NumericSuffix::Mm,
6655                }),
6656                y: Expr::Var(Number {
6657                    value: 0.0,
6658                    units: NumericSuffix::Mm,
6659                }),
6660            },
6661            construction: None,
6662        };
6663        let segment = SegmentCtor::Arc(arc_ctor);
6664        let (src_delta, scene_delta) = frontend
6665            .add_segment(&mock_ctx, version, sketch_id, segment, None)
6666            .await
6667            .unwrap();
6668        assert_eq!(
6669            src_delta.text.as_str(),
6670            "sketch001 = sketch(on = XY) {
6671  arc(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm], center = [var 10mm, var 0mm])
6672}
6673"
6674        );
6675        assert_eq!(
6676            scene_delta.new_objects,
6677            vec![ObjectId(2), ObjectId(3), ObjectId(4), ObjectId(5)]
6678        );
6679        for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
6680            assert_eq!(scene_object.id.0, i);
6681        }
6682        assert_eq!(scene_delta.new_graph.objects.len(), 6);
6683
6684        // The new objects are the end points, the center, and then the arc.
6685        let arc = *scene_delta.new_objects.last().unwrap();
6686
6687        let arc_ctor = ArcCtor {
6688            start: Point2d {
6689                x: Expr::Var(Number {
6690                    value: 1.0,
6691                    units: NumericSuffix::Mm,
6692                }),
6693                y: Expr::Var(Number {
6694                    value: 2.0,
6695                    units: NumericSuffix::Mm,
6696                }),
6697            },
6698            end: Point2d {
6699                x: Expr::Var(Number {
6700                    value: 13.0,
6701                    units: NumericSuffix::Mm,
6702                }),
6703                y: Expr::Var(Number {
6704                    value: 14.0,
6705                    units: NumericSuffix::Mm,
6706                }),
6707            },
6708            center: Point2d {
6709                x: Expr::Var(Number {
6710                    value: 13.0,
6711                    units: NumericSuffix::Mm,
6712                }),
6713                y: Expr::Var(Number {
6714                    value: 2.0,
6715                    units: NumericSuffix::Mm,
6716                }),
6717            },
6718            construction: None,
6719        };
6720        let segments = vec![ExistingSegmentCtor {
6721            id: arc,
6722            ctor: SegmentCtor::Arc(arc_ctor),
6723        }];
6724        let (src_delta, scene_delta) = frontend
6725            .edit_segments(&mock_ctx, version, sketch_id, segments)
6726            .await
6727            .unwrap();
6728        assert_eq!(
6729            src_delta.text.as_str(),
6730            "sketch001 = sketch(on = XY) {
6731  arc(start = [var 1mm, var 2mm], end = [var 13mm, var 14mm], center = [var 13mm, var 2mm])
6732}
6733"
6734        );
6735        assert_eq!(scene_delta.new_objects, vec![]);
6736        assert_eq!(scene_delta.new_graph.objects.len(), 6);
6737
6738        ctx.close().await;
6739        mock_ctx.close().await;
6740    }
6741
6742    #[tokio::test(flavor = "multi_thread")]
6743    async fn test_new_sketch_add_circle_edit_circle() {
6744        let program = Program::empty();
6745
6746        let mut frontend = FrontendState::new();
6747        frontend.program = program;
6748
6749        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6750        let mock_ctx = ExecutorContext::new_mock(None).await;
6751        let version = Version(0);
6752
6753        let sketch_args = SketchCtor {
6754            on: Plane::Default(PlaneName::Xy),
6755        };
6756        let (_src_delta, _scene_delta, sketch_id) = frontend
6757            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
6758            .await
6759            .unwrap();
6760
6761        // Add a circle segment.
6762        let circle_ctor = CircleCtor {
6763            start: Point2d {
6764                x: Expr::Var(Number {
6765                    value: 5.0,
6766                    units: NumericSuffix::Mm,
6767                }),
6768                y: Expr::Var(Number {
6769                    value: 0.0,
6770                    units: NumericSuffix::Mm,
6771                }),
6772            },
6773            center: Point2d {
6774                x: Expr::Var(Number {
6775                    value: 0.0,
6776                    units: NumericSuffix::Mm,
6777                }),
6778                y: Expr::Var(Number {
6779                    value: 0.0,
6780                    units: NumericSuffix::Mm,
6781                }),
6782            },
6783            construction: None,
6784        };
6785        let segment = SegmentCtor::Circle(circle_ctor);
6786        let (src_delta, scene_delta) = frontend
6787            .add_segment(&mock_ctx, version, sketch_id, segment, None)
6788            .await
6789            .unwrap();
6790        assert_eq!(
6791            src_delta.text.as_str(),
6792            "sketch001 = sketch(on = XY) {
6793  circle1 = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
6794}
6795"
6796        );
6797        // The new objects are start, center, and then the circle segment.
6798        assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
6799        assert_eq!(scene_delta.new_graph.objects.len(), 5);
6800
6801        let circle = *scene_delta.new_objects.last().unwrap();
6802
6803        // Edit the circle segment.
6804        let circle_ctor = CircleCtor {
6805            start: Point2d {
6806                x: Expr::Var(Number {
6807                    value: 10.0,
6808                    units: NumericSuffix::Mm,
6809                }),
6810                y: Expr::Var(Number {
6811                    value: 0.0,
6812                    units: NumericSuffix::Mm,
6813                }),
6814            },
6815            center: Point2d {
6816                x: Expr::Var(Number {
6817                    value: 3.0,
6818                    units: NumericSuffix::Mm,
6819                }),
6820                y: Expr::Var(Number {
6821                    value: 4.0,
6822                    units: NumericSuffix::Mm,
6823                }),
6824            },
6825            construction: None,
6826        };
6827        let segments = vec![ExistingSegmentCtor {
6828            id: circle,
6829            ctor: SegmentCtor::Circle(circle_ctor),
6830        }];
6831        let (src_delta, scene_delta) = frontend
6832            .edit_segments(&mock_ctx, version, sketch_id, segments)
6833            .await
6834            .unwrap();
6835        assert_eq!(
6836            src_delta.text.as_str(),
6837            "sketch001 = sketch(on = XY) {
6838  circle1 = circle(start = [var 10mm, var 0mm], center = [var 3mm, var 4mm])
6839}
6840"
6841        );
6842        assert_eq!(scene_delta.new_objects, vec![]);
6843        assert_eq!(scene_delta.new_graph.objects.len(), 5);
6844
6845        ctx.close().await;
6846        mock_ctx.close().await;
6847    }
6848
6849    #[tokio::test(flavor = "multi_thread")]
6850    async fn test_delete_circle() {
6851        let initial_source = "sketch001 = sketch(on = XY) {
6852  circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
6853}
6854";
6855
6856        let program = Program::parse(initial_source).unwrap().0.unwrap();
6857        let mut frontend = FrontendState::new();
6858
6859        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6860        let mock_ctx = ExecutorContext::new_mock(None).await;
6861        let version = Version(0);
6862
6863        frontend.hack_set_program(&ctx, program).await.unwrap();
6864        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
6865        let sketch_id = sketch_object.id;
6866        let sketch = expect_sketch(sketch_object);
6867
6868        // The sketch should have 3 segments: start point, center point, and the circle.
6869        assert_eq!(sketch.segments.len(), 3);
6870        let circle_id = sketch.segments[2];
6871
6872        // Delete the circle.
6873        let (src_delta, scene_delta) = frontend
6874            .delete_objects(&mock_ctx, version, sketch_id, vec![], vec![circle_id])
6875            .await
6876            .unwrap();
6877        assert_eq!(
6878            src_delta.text.as_str(),
6879            "sketch001 = sketch(on = XY) {
6880}
6881"
6882        );
6883        let new_sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
6884        let new_sketch = expect_sketch(new_sketch_object);
6885        assert_eq!(new_sketch.segments.len(), 0);
6886
6887        ctx.close().await;
6888        mock_ctx.close().await;
6889    }
6890
6891    #[tokio::test(flavor = "multi_thread")]
6892    async fn test_edit_circle_via_point() {
6893        let initial_source = "sketch001 = sketch(on = XY) {
6894  circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
6895}
6896";
6897
6898        let program = Program::parse(initial_source).unwrap().0.unwrap();
6899        let mut frontend = FrontendState::new();
6900
6901        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6902        let mock_ctx = ExecutorContext::new_mock(None).await;
6903        let version = Version(0);
6904
6905        frontend.hack_set_program(&ctx, program).await.unwrap();
6906        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
6907        let sketch_id = sketch_object.id;
6908        let sketch = expect_sketch(sketch_object);
6909
6910        // Find the circle segment and its start point.
6911        let circle_id = sketch
6912            .segments
6913            .iter()
6914            .copied()
6915            .find(|seg_id| {
6916                matches!(
6917                    &frontend.scene_graph.objects[seg_id.0].kind,
6918                    ObjectKind::Segment {
6919                        segment: Segment::Circle(_)
6920                    }
6921                )
6922            })
6923            .expect("Expected a circle segment in sketch");
6924        let circle_object = &frontend.scene_graph.objects[circle_id.0];
6925        let ObjectKind::Segment {
6926            segment: Segment::Circle(circle),
6927        } = &circle_object.kind
6928        else {
6929            panic!("Expected circle segment, got: {:?}", circle_object.kind);
6930        };
6931        let start_point_id = circle.start;
6932
6933        // Edit the start point via SegmentCtor::Point.
6934        let segments = vec![ExistingSegmentCtor {
6935            id: start_point_id,
6936            ctor: SegmentCtor::Point(PointCtor {
6937                position: Point2d {
6938                    x: Expr::Var(Number {
6939                        value: 7.0,
6940                        units: NumericSuffix::Mm,
6941                    }),
6942                    y: Expr::Var(Number {
6943                        value: 1.0,
6944                        units: NumericSuffix::Mm,
6945                    }),
6946                },
6947            }),
6948        }];
6949        let (src_delta, _scene_delta) = frontend
6950            .edit_segments(&mock_ctx, version, sketch_id, segments)
6951            .await
6952            .unwrap();
6953        assert_eq!(
6954            src_delta.text.as_str(),
6955            "sketch001 = sketch(on = XY) {
6956  circle(start = [var 7mm, var 1mm], center = [var 0mm, var 0mm])
6957}
6958"
6959        );
6960
6961        ctx.close().await;
6962        mock_ctx.close().await;
6963    }
6964
6965    #[tokio::test(flavor = "multi_thread")]
6966    async fn test_add_line_when_sketch_block_uses_variable() {
6967        let initial_source = "s = sketch(on = XY) {}
6968";
6969
6970        let program = Program::parse(initial_source).unwrap().0.unwrap();
6971
6972        let mut frontend = FrontendState::new();
6973
6974        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6975        let mock_ctx = ExecutorContext::new_mock(None).await;
6976        let version = Version(0);
6977
6978        frontend.hack_set_program(&ctx, program).await.unwrap();
6979        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
6980        let sketch_id = sketch_object.id;
6981
6982        let line_ctor = LineCtor {
6983            start: Point2d {
6984                x: Expr::Number(Number {
6985                    value: 0.0,
6986                    units: NumericSuffix::Mm,
6987                }),
6988                y: Expr::Number(Number {
6989                    value: 0.0,
6990                    units: NumericSuffix::Mm,
6991                }),
6992            },
6993            end: Point2d {
6994                x: Expr::Number(Number {
6995                    value: 10.0,
6996                    units: NumericSuffix::Mm,
6997                }),
6998                y: Expr::Number(Number {
6999                    value: 10.0,
7000                    units: NumericSuffix::Mm,
7001                }),
7002            },
7003            construction: None,
7004        };
7005        let segment = SegmentCtor::Line(line_ctor);
7006        let (src_delta, scene_delta) = frontend
7007            .add_segment(&mock_ctx, version, sketch_id, segment, None)
7008            .await
7009            .unwrap();
7010        assert_eq!(
7011            src_delta.text.as_str(),
7012            "s = sketch(on = XY) {
7013  line(start = [0mm, 0mm], end = [10mm, 10mm])
7014}
7015"
7016        );
7017        assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
7018        assert_eq!(scene_delta.new_graph.objects.len(), 5);
7019
7020        ctx.close().await;
7021        mock_ctx.close().await;
7022    }
7023
7024    #[tokio::test(flavor = "multi_thread")]
7025    async fn test_new_sketch_add_line_delete_sketch() {
7026        let program = Program::empty();
7027
7028        let mut frontend = FrontendState::new();
7029        frontend.program = program;
7030
7031        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7032        let mock_ctx = ExecutorContext::new_mock(None).await;
7033        let version = Version(0);
7034
7035        let sketch_args = SketchCtor {
7036            on: Plane::Default(PlaneName::Xy),
7037        };
7038        let (_src_delta, scene_delta, sketch_id) = frontend
7039            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
7040            .await
7041            .unwrap();
7042        assert_eq!(sketch_id, ObjectId(1));
7043        assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
7044        let sketch_object = &scene_delta.new_graph.objects[1];
7045        assert_eq!(sketch_object.id, ObjectId(1));
7046        assert_eq!(
7047            sketch_object.kind,
7048            ObjectKind::Sketch(Sketch {
7049                args: SketchCtor {
7050                    on: Plane::Default(PlaneName::Xy)
7051                },
7052                plane: ObjectId(0),
7053                segments: vec![],
7054                constraints: vec![],
7055            })
7056        );
7057        assert_eq!(scene_delta.new_graph.objects.len(), 2);
7058
7059        let line_ctor = LineCtor {
7060            start: Point2d {
7061                x: Expr::Number(Number {
7062                    value: 0.0,
7063                    units: NumericSuffix::Mm,
7064                }),
7065                y: Expr::Number(Number {
7066                    value: 0.0,
7067                    units: NumericSuffix::Mm,
7068                }),
7069            },
7070            end: Point2d {
7071                x: Expr::Number(Number {
7072                    value: 10.0,
7073                    units: NumericSuffix::Mm,
7074                }),
7075                y: Expr::Number(Number {
7076                    value: 10.0,
7077                    units: NumericSuffix::Mm,
7078                }),
7079            },
7080            construction: None,
7081        };
7082        let segment = SegmentCtor::Line(line_ctor);
7083        let (src_delta, scene_delta) = frontend
7084            .add_segment(&mock_ctx, version, sketch_id, segment, None)
7085            .await
7086            .unwrap();
7087        assert_eq!(
7088            src_delta.text.as_str(),
7089            "sketch001 = sketch(on = XY) {
7090  line(start = [0mm, 0mm], end = [10mm, 10mm])
7091}
7092"
7093        );
7094        assert_eq!(scene_delta.new_graph.objects.len(), 5);
7095
7096        let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
7097        assert_eq!(src_delta.text.as_str(), "");
7098        assert_eq!(scene_delta.new_graph.objects.len(), 0);
7099
7100        ctx.close().await;
7101        mock_ctx.close().await;
7102    }
7103
7104    #[tokio::test(flavor = "multi_thread")]
7105    async fn test_delete_sketch_when_sketch_block_uses_variable() {
7106        let initial_source = "s = sketch(on = XY) {}
7107";
7108
7109        let program = Program::parse(initial_source).unwrap().0.unwrap();
7110
7111        let mut frontend = FrontendState::new();
7112
7113        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7114        let mock_ctx = ExecutorContext::new_mock(None).await;
7115        let version = Version(0);
7116
7117        frontend.hack_set_program(&ctx, program).await.unwrap();
7118        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7119        let sketch_id = sketch_object.id;
7120
7121        let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
7122        assert_eq!(src_delta.text.as_str(), "");
7123        assert_eq!(scene_delta.new_graph.objects.len(), 0);
7124
7125        ctx.close().await;
7126        mock_ctx.close().await;
7127    }
7128
7129    #[tokio::test(flavor = "multi_thread")]
7130    async fn test_delete_sketch_after_comment() {
7131        let initial_source = "sketch001 = sketch(on = XZ) {
7132}
7133";
7134
7135        let program = Program::parse(initial_source).unwrap().0.unwrap();
7136        let mut frontend = FrontendState::new();
7137
7138        let ctx = ExecutorContext::new_with_engine(
7139            std::sync::Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().unwrap())),
7140            Default::default(),
7141        );
7142        let version = Version(0);
7143
7144        frontend.hack_set_program(&ctx, program).await.unwrap();
7145        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7146        let sketch_id = sketch_object.id;
7147        let original_source = sketch_object.source.clone();
7148
7149        let commented_source = "// test 1
7150sketch001 = sketch(on = XZ) {
7151}
7152";
7153        let commented_program = Program::parse(commented_source).unwrap().0.unwrap();
7154        frontend.engine_execute(&ctx, commented_program).await.unwrap();
7155
7156        let cached_sketch_object = &frontend.scene_graph.objects[sketch_id.0];
7157        assert_eq!(cached_sketch_object.source, original_source);
7158
7159        let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
7160        assert!(
7161            !src_delta.text.contains("sketch001"),
7162            "sketch was not deleted: {}",
7163            src_delta.text
7164        );
7165        // The leading line comment must survive deletion.
7166        assert_eq!(src_delta.text.as_str(), "// test 1\n");
7167        assert_eq!(scene_delta.new_graph.objects.len(), 0);
7168
7169        ctx.close().await;
7170    }
7171
7172    #[tokio::test(flavor = "multi_thread")]
7173    async fn test_delete_sketch_preserves_pre_comment_when_followed_by_code() {
7174        let initial_source = "sketch001 = sketch(on = XZ) {
7175}
7176foo = 1
7177";
7178
7179        let program = Program::parse(initial_source).unwrap().0.unwrap();
7180        let mut frontend = FrontendState::new();
7181
7182        let ctx = ExecutorContext::new_with_engine(
7183            std::sync::Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().unwrap())),
7184            Default::default(),
7185        );
7186        let version = Version(0);
7187
7188        frontend.hack_set_program(&ctx, program).await.unwrap();
7189        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7190        let sketch_id = sketch_object.id;
7191
7192        let commented_source = "// keep me
7193sketch001 = sketch(on = XZ) {
7194}
7195foo = 1
7196";
7197        let commented_program = Program::parse(commented_source).unwrap().0.unwrap();
7198        frontend.engine_execute(&ctx, commented_program).await.unwrap();
7199
7200        let (src_delta, _scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
7201        // The leading comment should remain, now attached to the following body item.
7202        assert_eq!(src_delta.text.as_str(), "// keep me\nfoo = 1\n");
7203
7204        ctx.close().await;
7205    }
7206
7207    #[tokio::test(flavor = "multi_thread")]
7208    async fn test_delete_segment_preserves_pre_comment() {
7209        let initial_source = "\
7210sketch(on = XY) {
7211  point(at = [var 1, var 2])
7212  // describe the middle point
7213  point(at = [var 3, var 4])
7214  point(at = [var 5, var 6])
7215}
7216";
7217
7218        let program = Program::parse(initial_source).unwrap().0.unwrap();
7219        let mut frontend = FrontendState::new();
7220
7221        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7222        let mock_ctx = ExecutorContext::new_mock(None).await;
7223        let version = Version(0);
7224
7225        frontend.hack_set_program(&ctx, program).await.unwrap();
7226        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7227        let sketch_id = sketch_object.id;
7228        let sketch = expect_sketch(sketch_object);
7229
7230        let middle_point_id = *sketch.segments.get(1).unwrap();
7231
7232        let (src_delta, _scene_delta) = frontend
7233            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![middle_point_id])
7234            .await
7235            .unwrap();
7236        // The line comment on the line above the deleted point must be preserved.
7237        // It is reattached to the next surviving body item.
7238        assert_eq!(
7239            src_delta.text.as_str(),
7240            "\
7241sketch(on = XY) {
7242  point(at = [var 1mm, var 2mm])
7243  // describe the middle point
7244  point(at = [var 5mm, var 6mm])
7245}
7246"
7247        );
7248
7249        ctx.close().await;
7250        mock_ctx.close().await;
7251    }
7252
7253    #[tokio::test(flavor = "multi_thread")]
7254    async fn test_delete_last_segment_preserves_pre_comment() {
7255        let initial_source = "\
7256sketch(on = XY) {
7257  point(at = [var 1, var 2])
7258  // describe the trailing point
7259  point(at = [var 3, var 4])
7260}
7261";
7262
7263        let program = Program::parse(initial_source).unwrap().0.unwrap();
7264        let mut frontend = FrontendState::new();
7265
7266        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7267        let mock_ctx = ExecutorContext::new_mock(None).await;
7268        let version = Version(0);
7269
7270        frontend.hack_set_program(&ctx, program).await.unwrap();
7271        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7272        let sketch_id = sketch_object.id;
7273        let sketch = expect_sketch(sketch_object);
7274
7275        let last_point_id = *sketch.segments.last().unwrap();
7276
7277        let (src_delta, _scene_delta) = frontend
7278            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![last_point_id])
7279            .await
7280            .unwrap();
7281        // No following item to attach to; the comment is kept inside the sketch
7282        // block as trailing non-code metadata so the user does not lose it.
7283        assert_eq!(
7284            src_delta.text.as_str(),
7285            "\
7286sketch(on = XY) {
7287  point(at = [var 1mm, var 2mm])
7288  // describe the trailing point
7289}
7290"
7291        );
7292
7293        ctx.close().await;
7294        mock_ctx.close().await;
7295    }
7296
7297    #[tokio::test(flavor = "multi_thread")]
7298    async fn test_delete_segment_drops_inline_trailing_comment() {
7299        let initial_source = "\
7300sketch(on = XY) {
7301  point(at = [var 1, var 2])
7302  point(at = [var 3, var 4]) // same-line note that gets dropped
7303  point(at = [var 5, var 6])
7304}
7305";
7306
7307        let program = Program::parse(initial_source).unwrap().0.unwrap();
7308        let mut frontend = FrontendState::new();
7309
7310        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7311        let mock_ctx = ExecutorContext::new_mock(None).await;
7312        let version = Version(0);
7313
7314        frontend.hack_set_program(&ctx, program).await.unwrap();
7315        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7316        let sketch_id = sketch_object.id;
7317        let sketch = expect_sketch(sketch_object);
7318
7319        let middle_point_id = *sketch.segments.get(1).unwrap();
7320
7321        let (src_delta, _scene_delta) = frontend
7322            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![middle_point_id])
7323            .await
7324            .unwrap();
7325        // The same-line trailing comment is removed along with the deleted code.
7326        assert!(
7327            !src_delta.text.contains("same-line note"),
7328            "inline comment should have been removed: {}",
7329            src_delta.text
7330        );
7331
7332        ctx.close().await;
7333        mock_ctx.close().await;
7334    }
7335
7336    #[tokio::test(flavor = "multi_thread")]
7337    async fn test_delete_segments_preserves_block_comments_across_positions() {
7338        // One test exercising several `delete_body_item_preserving_pre_comments`
7339        // branches at once with `/* ... */` block comments:
7340        //   - first point: leading block comment must migrate to the next item.
7341        //   - first point: same-line trailing block comment must be dropped.
7342        //   - middle point: leading block comment must stay attached after migration.
7343        //   - last point: leading block comment, with no surviving next item,
7344        //     must be converted into a trailing NonCodeNode.
7345        let initial_source = "\
7346sketch(on = XY) {
7347  /* above first - moves to middle */
7348  point(at = [var 1, var 2]) /* same-line on first - dropped */
7349  /* above middle - stays */
7350  point(at = [var 3, var 4])
7351  /* above last - moves to trailing meta */
7352  point(at = [var 5, var 6])
7353}
7354";
7355
7356        let program = Program::parse(initial_source).unwrap().0.unwrap();
7357        let mut frontend = FrontendState::new();
7358
7359        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7360        let mock_ctx = ExecutorContext::new_mock(None).await;
7361        let version = Version(0);
7362
7363        frontend.hack_set_program(&ctx, program).await.unwrap();
7364        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7365        let sketch_id = sketch_object.id;
7366        let sketch = expect_sketch(sketch_object);
7367
7368        let first_point_id = *sketch.segments.first().unwrap();
7369        let last_point_id = *sketch.segments.last().unwrap();
7370
7371        let (src_delta, _scene_delta) = frontend
7372            .delete_objects(
7373                &mock_ctx,
7374                version,
7375                sketch_id,
7376                Vec::new(),
7377                vec![first_point_id, last_point_id],
7378            )
7379            .await
7380            .unwrap();
7381        assert_eq!(
7382            src_delta.text.as_str(),
7383            "\
7384sketch(on = XY) {
7385  /* above first - moves to middle */
7386  /* above middle - stays */
7387  point(at = [var 3mm, var 4mm])
7388  /* above last - moves to trailing meta */
7389}
7390"
7391        );
7392
7393        ctx.close().await;
7394        mock_ctx.close().await;
7395    }
7396
7397    #[tokio::test(flavor = "multi_thread")]
7398    async fn test_edit_line_when_editing_its_start_point() {
7399        let initial_source = "\
7400sketch(on = XY) {
7401  line(start = [var 1, var 2], end = [var 3, var 4])
7402}
7403";
7404
7405        let program = Program::parse(initial_source).unwrap().0.unwrap();
7406
7407        let mut frontend = FrontendState::new();
7408
7409        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7410        let mock_ctx = ExecutorContext::new_mock(None).await;
7411        let version = Version(0);
7412
7413        frontend.hack_set_program(&ctx, program).await.unwrap();
7414        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7415        let sketch_id = sketch_object.id;
7416        let sketch = expect_sketch(sketch_object);
7417
7418        let point_id = *sketch.segments.first().unwrap();
7419
7420        let point_ctor = PointCtor {
7421            position: Point2d {
7422                x: Expr::Var(Number {
7423                    value: 5.0,
7424                    units: NumericSuffix::Inch,
7425                }),
7426                y: Expr::Var(Number {
7427                    value: 6.0,
7428                    units: NumericSuffix::Inch,
7429                }),
7430            },
7431        };
7432        let segments = vec![ExistingSegmentCtor {
7433            id: point_id,
7434            ctor: SegmentCtor::Point(point_ctor),
7435        }];
7436        let (src_delta, scene_delta) = frontend
7437            .edit_segments(&mock_ctx, version, sketch_id, segments)
7438            .await
7439            .unwrap();
7440        assert_eq!(
7441            src_delta.text.as_str(),
7442            "\
7443sketch(on = XY) {
7444  line(start = [var 127mm, var 152.4mm], end = [var 3mm, var 4mm])
7445}
7446"
7447        );
7448        assert_eq!(scene_delta.new_objects, vec![]);
7449        assert_eq!(scene_delta.new_graph.objects.len(), 5);
7450
7451        ctx.close().await;
7452        mock_ctx.close().await;
7453    }
7454
7455    #[tokio::test(flavor = "multi_thread")]
7456    async fn test_edit_line_when_editing_its_end_point() {
7457        let initial_source = "\
7458sketch(on = XY) {
7459  line(start = [var 1, var 2], end = [var 3, var 4])
7460}
7461";
7462
7463        let program = Program::parse(initial_source).unwrap().0.unwrap();
7464
7465        let mut frontend = FrontendState::new();
7466
7467        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7468        let mock_ctx = ExecutorContext::new_mock(None).await;
7469        let version = Version(0);
7470
7471        frontend.hack_set_program(&ctx, program).await.unwrap();
7472        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7473        let sketch_id = sketch_object.id;
7474        let sketch = expect_sketch(sketch_object);
7475        let point_id = *sketch.segments.get(1).unwrap();
7476
7477        let point_ctor = PointCtor {
7478            position: Point2d {
7479                x: Expr::Var(Number {
7480                    value: 5.0,
7481                    units: NumericSuffix::Inch,
7482                }),
7483                y: Expr::Var(Number {
7484                    value: 6.0,
7485                    units: NumericSuffix::Inch,
7486                }),
7487            },
7488        };
7489        let segments = vec![ExistingSegmentCtor {
7490            id: point_id,
7491            ctor: SegmentCtor::Point(point_ctor),
7492        }];
7493        let (src_delta, scene_delta) = frontend
7494            .edit_segments(&mock_ctx, version, sketch_id, segments)
7495            .await
7496            .unwrap();
7497        assert_eq!(
7498            src_delta.text.as_str(),
7499            "\
7500sketch(on = XY) {
7501  line(start = [var 1mm, var 2mm], end = [var 127mm, var 152.4mm])
7502}
7503"
7504        );
7505        assert_eq!(scene_delta.new_objects, vec![]);
7506        assert_eq!(
7507            scene_delta.new_graph.objects.len(),
7508            5,
7509            "{:#?}",
7510            scene_delta.new_graph.objects
7511        );
7512
7513        ctx.close().await;
7514        mock_ctx.close().await;
7515    }
7516
7517    #[tokio::test(flavor = "multi_thread")]
7518    async fn test_edit_line_with_coincident_feedback() {
7519        let initial_source = "\
7520sketch(on = XY) {
7521  line1 = line(start = [var 1, var 2], end = [var 1, var 2])
7522  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
7523  fixed([line1.start, [0, 0]])
7524  coincident([line1.end, line2.start])
7525  equalLength([line1, line2])
7526}
7527";
7528
7529        let program = Program::parse(initial_source).unwrap().0.unwrap();
7530
7531        let mut frontend = FrontendState::new();
7532
7533        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7534        let mock_ctx = ExecutorContext::new_mock(None).await;
7535        let version = Version(0);
7536
7537        frontend.hack_set_program(&ctx, program).await.unwrap();
7538        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7539        let sketch_id = sketch_object.id;
7540        let sketch = expect_sketch(sketch_object);
7541        let line2_end_id = *sketch.segments.get(4).unwrap();
7542
7543        let segments = vec![ExistingSegmentCtor {
7544            id: line2_end_id,
7545            ctor: SegmentCtor::Point(PointCtor {
7546                position: Point2d {
7547                    x: Expr::Var(Number {
7548                        value: 9.0,
7549                        units: NumericSuffix::None,
7550                    }),
7551                    y: Expr::Var(Number {
7552                        value: 10.0,
7553                        units: NumericSuffix::None,
7554                    }),
7555                },
7556            }),
7557        }];
7558        let (src_delta, scene_delta) = frontend
7559            .edit_segments(&mock_ctx, version, sketch_id, segments)
7560            .await
7561            .unwrap();
7562        assert_eq!(
7563            src_delta.text.as_str(),
7564            "\
7565sketch(on = XY) {
7566  line1 = line(start = [var 0mm, var 0mm], end = [var 4.14mm, var 5.32mm])
7567  line2 = line(start = [var 4.14mm, var 5.32mm], end = [var 9mm, var 10mm])
7568  fixed([line1.start, [0, 0]])
7569  coincident([line1.end, line2.start])
7570  equalLength([line1, line2])
7571}
7572"
7573        );
7574        assert_eq!(
7575            scene_delta.new_graph.objects.len(),
7576            11,
7577            "{:#?}",
7578            scene_delta.new_graph.objects
7579        );
7580
7581        ctx.close().await;
7582        mock_ctx.close().await;
7583    }
7584
7585    #[tokio::test(flavor = "multi_thread")]
7586    async fn test_delete_point_without_var() {
7587        let initial_source = "\
7588sketch(on = XY) {
7589  point(at = [var 1, var 2])
7590  point(at = [var 3, var 4])
7591  point(at = [var 5, var 6])
7592}
7593";
7594
7595        let program = Program::parse(initial_source).unwrap().0.unwrap();
7596
7597        let mut frontend = FrontendState::new();
7598
7599        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7600        let mock_ctx = ExecutorContext::new_mock(None).await;
7601        let version = Version(0);
7602
7603        frontend.hack_set_program(&ctx, program).await.unwrap();
7604        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7605        let sketch_id = sketch_object.id;
7606        let sketch = expect_sketch(sketch_object);
7607
7608        let point_id = *sketch.segments.get(1).unwrap();
7609
7610        let (src_delta, scene_delta) = frontend
7611            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
7612            .await
7613            .unwrap();
7614        assert_eq!(
7615            src_delta.text.as_str(),
7616            "\
7617sketch(on = XY) {
7618  point(at = [var 1mm, var 2mm])
7619  point(at = [var 5mm, var 6mm])
7620}
7621"
7622        );
7623        assert_eq!(scene_delta.new_objects, vec![]);
7624        assert_eq!(scene_delta.new_graph.objects.len(), 4);
7625
7626        ctx.close().await;
7627        mock_ctx.close().await;
7628    }
7629
7630    #[tokio::test(flavor = "multi_thread")]
7631    async fn test_delete_point_with_var() {
7632        let initial_source = "\
7633sketch(on = XY) {
7634  point(at = [var 1, var 2])
7635  point1 = point(at = [var 3, var 4])
7636  point(at = [var 5, var 6])
7637}
7638";
7639
7640        let program = Program::parse(initial_source).unwrap().0.unwrap();
7641
7642        let mut frontend = FrontendState::new();
7643
7644        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7645        let mock_ctx = ExecutorContext::new_mock(None).await;
7646        let version = Version(0);
7647
7648        frontend.hack_set_program(&ctx, program).await.unwrap();
7649        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7650        let sketch_id = sketch_object.id;
7651        let sketch = expect_sketch(sketch_object);
7652
7653        let point_id = *sketch.segments.get(1).unwrap();
7654
7655        let (src_delta, scene_delta) = frontend
7656            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
7657            .await
7658            .unwrap();
7659        assert_eq!(
7660            src_delta.text.as_str(),
7661            "\
7662sketch(on = XY) {
7663  point(at = [var 1mm, var 2mm])
7664  point(at = [var 5mm, var 6mm])
7665}
7666"
7667        );
7668        assert_eq!(scene_delta.new_objects, vec![]);
7669        assert_eq!(scene_delta.new_graph.objects.len(), 4);
7670
7671        ctx.close().await;
7672        mock_ctx.close().await;
7673    }
7674
7675    #[tokio::test(flavor = "multi_thread")]
7676    async fn test_delete_multiple_points() {
7677        let initial_source = "\
7678sketch(on = XY) {
7679  point(at = [var 1, var 2])
7680  point1 = point(at = [var 3, var 4])
7681  point(at = [var 5, var 6])
7682}
7683";
7684
7685        let program = Program::parse(initial_source).unwrap().0.unwrap();
7686
7687        let mut frontend = FrontendState::new();
7688
7689        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7690        let mock_ctx = ExecutorContext::new_mock(None).await;
7691        let version = Version(0);
7692
7693        frontend.hack_set_program(&ctx, program).await.unwrap();
7694        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7695        let sketch_id = sketch_object.id;
7696
7697        let sketch = expect_sketch(sketch_object);
7698
7699        let point1_id = *sketch.segments.first().unwrap();
7700        let point2_id = *sketch.segments.get(1).unwrap();
7701
7702        let (src_delta, scene_delta) = frontend
7703            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point1_id, point2_id])
7704            .await
7705            .unwrap();
7706        assert_eq!(
7707            src_delta.text.as_str(),
7708            "\
7709sketch(on = XY) {
7710  point(at = [var 5mm, var 6mm])
7711}
7712"
7713        );
7714        assert_eq!(scene_delta.new_objects, vec![]);
7715        assert_eq!(scene_delta.new_graph.objects.len(), 3);
7716
7717        ctx.close().await;
7718        mock_ctx.close().await;
7719    }
7720
7721    #[tokio::test(flavor = "multi_thread")]
7722    async fn test_delete_coincident_constraint() {
7723        let initial_source = "\
7724sketch(on = XY) {
7725  point1 = point(at = [var 1, var 2])
7726  point2 = point(at = [var 3, var 4])
7727  coincident([point1, point2])
7728  point(at = [var 5, var 6])
7729}
7730";
7731
7732        let program = Program::parse(initial_source).unwrap().0.unwrap();
7733
7734        let mut frontend = FrontendState::new();
7735
7736        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7737        let mock_ctx = ExecutorContext::new_mock(None).await;
7738        let version = Version(0);
7739
7740        frontend.hack_set_program(&ctx, program).await.unwrap();
7741        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7742        let sketch_id = sketch_object.id;
7743        let sketch = expect_sketch(sketch_object);
7744
7745        let coincident_id = *sketch.constraints.first().unwrap();
7746
7747        let (src_delta, scene_delta) = frontend
7748            .delete_objects(&mock_ctx, version, sketch_id, vec![coincident_id], Vec::new())
7749            .await
7750            .unwrap();
7751        assert_eq!(
7752            src_delta.text.as_str(),
7753            "\
7754sketch(on = XY) {
7755  point1 = point(at = [var 1mm, var 2mm])
7756  point2 = point(at = [var 3mm, var 4mm])
7757  point(at = [var 5mm, var 6mm])
7758}
7759"
7760        );
7761        assert_eq!(scene_delta.new_objects, vec![]);
7762        assert_eq!(scene_delta.new_graph.objects.len(), 5);
7763
7764        ctx.close().await;
7765        mock_ctx.close().await;
7766    }
7767
7768    #[tokio::test(flavor = "multi_thread")]
7769    async fn test_delete_line_cascades_to_coincident_constraint() {
7770        let initial_source = "\
7771sketch(on = XY) {
7772  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
7773  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
7774  coincident([line1.end, line2.start])
7775}
7776";
7777
7778        let program = Program::parse(initial_source).unwrap().0.unwrap();
7779
7780        let mut frontend = FrontendState::new();
7781
7782        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7783        let mock_ctx = ExecutorContext::new_mock(None).await;
7784        let version = Version(0);
7785
7786        frontend.hack_set_program(&ctx, program).await.unwrap();
7787        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7788        let sketch_id = sketch_object.id;
7789        let sketch = expect_sketch(sketch_object);
7790        let line_id = *sketch.segments.get(5).unwrap();
7791
7792        let (src_delta, scene_delta) = frontend
7793            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
7794            .await
7795            .unwrap();
7796        assert_eq!(
7797            src_delta.text.as_str(),
7798            "\
7799sketch(on = XY) {
7800  line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
7801}
7802"
7803        );
7804        assert_eq!(
7805            scene_delta.new_graph.objects.len(),
7806            5,
7807            "{:#?}",
7808            scene_delta.new_graph.objects
7809        );
7810
7811        ctx.close().await;
7812        mock_ctx.close().await;
7813    }
7814
7815    #[tokio::test(flavor = "multi_thread")]
7816    async fn test_delete_line_cascades_to_distance_constraint() {
7817        let initial_source = "\
7818sketch(on = XY) {
7819  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
7820  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
7821  distance([line1.end, line2.start]) == 10mm
7822}
7823";
7824
7825        let program = Program::parse(initial_source).unwrap().0.unwrap();
7826
7827        let mut frontend = FrontendState::new();
7828
7829        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7830        let mock_ctx = ExecutorContext::new_mock(None).await;
7831        let version = Version(0);
7832
7833        frontend.hack_set_program(&ctx, program).await.unwrap();
7834        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7835        let sketch_id = sketch_object.id;
7836        let sketch = expect_sketch(sketch_object);
7837        let line_id = *sketch.segments.get(5).unwrap();
7838
7839        let (src_delta, scene_delta) = frontend
7840            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
7841            .await
7842            .unwrap();
7843        assert_eq!(
7844            src_delta.text.as_str(),
7845            "\
7846sketch(on = XY) {
7847  line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
7848}
7849"
7850        );
7851        assert_eq!(
7852            scene_delta.new_graph.objects.len(),
7853            5,
7854            "{:#?}",
7855            scene_delta.new_graph.objects
7856        );
7857
7858        ctx.close().await;
7859        mock_ctx.close().await;
7860    }
7861
7862    #[tokio::test(flavor = "multi_thread")]
7863    async fn test_delete_point_cascades_to_horizontal_distance_constraint() {
7864        let initial_source = "\
7865sketch(on = XY) {
7866  point1 = point(at = [var 1, var 2])
7867  point2 = point(at = [var 3, var 4])
7868  horizontalDistance([point1, point2]) == 10mm
7869}
7870";
7871
7872        let program = Program::parse(initial_source).unwrap().0.unwrap();
7873
7874        let mut frontend = FrontendState::new();
7875
7876        let mock_ctx = ExecutorContext::new_mock(None).await;
7877        let version = Version(0);
7878
7879        frontend.program = program.clone();
7880        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
7881        frontend.update_state_after_exec(outcome, true);
7882        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7883        let sketch_id = sketch_object.id;
7884        let sketch = expect_sketch(sketch_object);
7885        let point2_id = *sketch.segments.get(1).unwrap();
7886
7887        let (src_delta, scene_delta) = frontend
7888            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point2_id])
7889            .await
7890            .unwrap();
7891        assert_eq!(
7892            src_delta.text.as_str(),
7893            "\
7894sketch(on = XY) {
7895  point1 = point(at = [var 1mm, var 2mm])
7896}
7897"
7898        );
7899        assert_eq!(
7900            scene_delta.new_graph.objects.len(),
7901            3,
7902            "{:#?}",
7903            scene_delta.new_graph.objects
7904        );
7905
7906        mock_ctx.close().await;
7907    }
7908
7909    #[tokio::test(flavor = "multi_thread")]
7910    async fn test_delete_line_cascades_to_fixed_constraint() {
7911        let initial_source = "\
7912sketch(on = XY) {
7913  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
7914  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
7915  fixed([line1.start, [0, 0]])
7916}
7917";
7918
7919        let program = Program::parse(initial_source).unwrap().0.unwrap();
7920
7921        let mut frontend = FrontendState::new();
7922
7923        let mock_ctx = ExecutorContext::new_mock(None).await;
7924        let version = Version(0);
7925
7926        frontend.program = program.clone();
7927        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
7928        frontend.update_state_after_exec(outcome, true);
7929        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7930        let sketch_id = sketch_object.id;
7931        let sketch = expect_sketch(sketch_object);
7932        let line1_id = *sketch.segments.get(2).unwrap();
7933
7934        let (src_delta, scene_delta) = frontend
7935            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
7936            .await
7937            .unwrap();
7938        assert_eq!(
7939            src_delta.text.as_str(),
7940            "\
7941sketch(on = XY) {
7942  line2 = line(start = [var 5mm, var 6mm], end = [var 7mm, var 8mm])
7943}
7944"
7945        );
7946        assert_eq!(
7947            scene_delta.new_graph.objects.len(),
7948            5,
7949            "{:#?}",
7950            scene_delta.new_graph.objects
7951        );
7952
7953        mock_ctx.close().await;
7954    }
7955
7956    #[tokio::test(flavor = "multi_thread")]
7957    async fn test_delete_line_cascades_to_midpoint_constraint() {
7958        let initial_source = "\
7959sketch(on = XY) {
7960  point1 = point(at = [var 1, var 2])
7961  line1 = line(start = [var 0, var 0], end = [var 6, var 4])
7962  midpoint(line1, point = point1)
7963}
7964";
7965
7966        let program = Program::parse(initial_source).unwrap().0.unwrap();
7967
7968        let mut frontend = FrontendState::new();
7969
7970        let mock_ctx = ExecutorContext::new_mock(None).await;
7971        let version = Version(0);
7972
7973        frontend.program = program.clone();
7974        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
7975        frontend.update_state_after_exec(outcome, true);
7976        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7977        let sketch_id = sketch_object.id;
7978        let sketch = expect_sketch(sketch_object);
7979        let line1_id = *sketch.segments.get(3).unwrap();
7980
7981        let (src_delta, scene_delta) = frontend
7982            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
7983            .await
7984            .unwrap();
7985        assert_eq!(
7986            src_delta.text.as_str(),
7987            "\
7988sketch(on = XY) {
7989  point1 = point(at = [var 1mm, var 2mm])
7990}
7991"
7992        );
7993        assert_eq!(
7994            scene_delta.new_graph.objects.len(),
7995            3,
7996            "{:#?}",
7997            scene_delta.new_graph.objects
7998        );
7999
8000        mock_ctx.close().await;
8001    }
8002
8003    #[tokio::test(flavor = "multi_thread")]
8004    async fn test_delete_point_preserves_multiline_coincident_constraint() {
8005        let initial_source = "\
8006sketch(on = XY) {
8007  point1 = point(at = [var 1, var 2])
8008  point2 = point(at = [var 3, var 4])
8009  point3 = point(at = [var 5, var 6])
8010  coincident([point1, point2, point3])
8011}
8012";
8013
8014        let program = Program::parse(initial_source).unwrap().0.unwrap();
8015
8016        let mut frontend = FrontendState::new();
8017
8018        let mock_ctx = ExecutorContext::new_mock(None).await;
8019        let version = Version(0);
8020
8021        frontend.program = program.clone();
8022        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
8023        frontend.update_state_after_exec(outcome, true);
8024        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8025        let sketch_id = sketch_object.id;
8026        let sketch = expect_sketch(sketch_object);
8027        let point3_id = *sketch.segments.get(2).unwrap();
8028
8029        let (src_delta, scene_delta) = frontend
8030            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point3_id])
8031            .await
8032            .unwrap();
8033        assert!(src_delta.text.contains("point1 = point("), "{}", src_delta.text);
8034        assert!(src_delta.text.contains("point2 = point("), "{}", src_delta.text);
8035        assert!(!src_delta.text.contains("point3 = point("), "{}", src_delta.text);
8036        assert!(
8037            src_delta.text.contains("coincident([point1, point2])"),
8038            "{}",
8039            src_delta.text
8040        );
8041
8042        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8043        let sketch = expect_sketch(sketch_object);
8044        assert_eq!(sketch.segments.len(), 2);
8045        assert_eq!(sketch.constraints.len(), 1);
8046
8047        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
8048        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
8049            panic!("Expected constraint object");
8050        };
8051        let Constraint::Coincident(coincident) = constraint else {
8052            panic!("Expected coincident constraint");
8053        };
8054        assert_eq!(
8055            coincident.segments,
8056            sketch
8057                .segments
8058                .iter()
8059                .copied()
8060                .map(Into::into)
8061                .collect::<Vec<ConstraintSegment>>()
8062        );
8063
8064        mock_ctx.close().await;
8065    }
8066
8067    #[tokio::test(flavor = "multi_thread")]
8068    async fn test_delete_line_preserves_multiline_equal_length_constraint() {
8069        let initial_source = "\
8070sketch(on = XY) {
8071  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8072  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8073  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
8074  equalLength([line1, line2, line3])
8075}
8076";
8077
8078        let program = Program::parse(initial_source).unwrap().0.unwrap();
8079
8080        let mut frontend = FrontendState::new();
8081
8082        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8083        let mock_ctx = ExecutorContext::new_mock(None).await;
8084        let version = Version(0);
8085
8086        frontend.hack_set_program(&ctx, program).await.unwrap();
8087        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8088        let sketch_id = sketch_object.id;
8089        let sketch = expect_sketch(sketch_object);
8090        let line3_id = *sketch.segments.get(8).unwrap();
8091
8092        let (src_delta, scene_delta) = frontend
8093            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line3_id])
8094            .await
8095            .unwrap();
8096        assert_eq!(
8097            src_delta.text.as_str(),
8098            "\
8099sketch(on = XY) {
8100  line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
8101  line2 = line(start = [var 5mm, var 6mm], end = [var 7mm, var 8mm])
8102  equalLength([line1, line2])
8103}
8104"
8105        );
8106
8107        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8108        let sketch = expect_sketch(sketch_object);
8109        assert_eq!(sketch.constraints.len(), 1);
8110
8111        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
8112        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
8113            panic!("Expected constraint object");
8114        };
8115        let Constraint::LinesEqualLength(lines_equal_length) = constraint else {
8116            panic!("Expected lines equal length constraint");
8117        };
8118        assert_eq!(lines_equal_length.lines.len(), 2);
8119
8120        ctx.close().await;
8121        mock_ctx.close().await;
8122    }
8123
8124    #[tokio::test(flavor = "multi_thread")]
8125    async fn test_delete_line_preserves_multiline_horizontal_constraint() {
8126        let initial_source = "\
8127sketch(on = XY) {
8128  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8129  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8130  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
8131  horizontal([line1.end, line2.start, line3.start])
8132}
8133";
8134
8135        let program = Program::parse(initial_source).unwrap().0.unwrap();
8136
8137        let mut frontend = FrontendState::new();
8138
8139        let mock_ctx = ExecutorContext::new_mock(None).await;
8140        let version = Version(0);
8141
8142        frontend.program = program.clone();
8143        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
8144        frontend.update_state_after_exec(outcome, true);
8145        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8146        let sketch_id = sketch_object.id;
8147        let sketch = expect_sketch(sketch_object);
8148        let line1_id = *sketch.segments.get(2).unwrap();
8149
8150        let (src_delta, scene_delta) = frontend
8151            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
8152            .await
8153            .unwrap();
8154        assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
8155        assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
8156        assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
8157        assert!(
8158            src_delta.text.contains("horizontal([line2.start, line3.start])"),
8159            "{}",
8160            src_delta.text
8161        );
8162
8163        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8164        let sketch = expect_sketch(sketch_object);
8165        assert_eq!(sketch.constraints.len(), 1);
8166
8167        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
8168        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
8169            panic!("Expected constraint object");
8170        };
8171        let Constraint::Horizontal(Horizontal::Points { points }) = constraint else {
8172            panic!("Expected horizontal points constraint");
8173        };
8174        let remaining_points = vec![sketch.segments[0].into(), sketch.segments[3].into()];
8175        assert_eq!(*points, remaining_points);
8176
8177        mock_ctx.close().await;
8178    }
8179
8180    #[tokio::test(flavor = "multi_thread")]
8181    async fn test_delete_line_preserves_multiline_vertical_constraint() {
8182        let initial_source = "\
8183sketch(on = XY) {
8184  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8185  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8186  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
8187  vertical([line1.end, line2.start, line3.start])
8188}
8189";
8190
8191        let program = Program::parse(initial_source).unwrap().0.unwrap();
8192
8193        let mut frontend = FrontendState::new();
8194
8195        let mock_ctx = ExecutorContext::new_mock(None).await;
8196        let version = Version(0);
8197
8198        frontend.program = program.clone();
8199        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
8200        frontend.update_state_after_exec(outcome, true);
8201        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8202        let sketch_id = sketch_object.id;
8203        let sketch = expect_sketch(sketch_object);
8204        let line1_id = *sketch.segments.get(2).unwrap();
8205
8206        let (src_delta, scene_delta) = frontend
8207            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
8208            .await
8209            .unwrap();
8210        assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
8211        assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
8212        assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
8213        assert!(
8214            src_delta.text.contains("vertical([line2.start, line3.start])"),
8215            "{}",
8216            src_delta.text
8217        );
8218
8219        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8220        let sketch = expect_sketch(sketch_object);
8221        assert_eq!(sketch.constraints.len(), 1);
8222
8223        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
8224        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
8225            panic!("Expected constraint object");
8226        };
8227        let Constraint::Vertical(Vertical::Points { points }) = constraint else {
8228            panic!("Expected vertical points constraint");
8229        };
8230        let remaining_points = vec![sketch.segments[0].into(), sketch.segments[3].into()];
8231        assert_eq!(*points, remaining_points);
8232
8233        mock_ctx.close().await;
8234    }
8235
8236    #[tokio::test(flavor = "multi_thread")]
8237    async fn test_delete_line_preserves_multiline_coincident_constraint() {
8238        let initial_source = "\
8239sketch(on = XY) {
8240  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8241  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8242  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
8243  coincident([line1.end, line2.start, line3.start])
8244}
8245";
8246
8247        let program = Program::parse(initial_source).unwrap().0.unwrap();
8248
8249        let mut frontend = FrontendState::new();
8250
8251        let mock_ctx = ExecutorContext::new_mock(None).await;
8252        let version = Version(0);
8253
8254        frontend.program = program.clone();
8255        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
8256        frontend.update_state_after_exec(outcome, true);
8257        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8258        let sketch_id = sketch_object.id;
8259        let sketch = expect_sketch(sketch_object);
8260        let line1_id = *sketch.segments.get(2).unwrap();
8261
8262        let (src_delta, scene_delta) = frontend
8263            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
8264            .await
8265            .unwrap();
8266        assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
8267        assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
8268        assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
8269        assert!(
8270            src_delta.text.contains("coincident([line2.start, line3.start])"),
8271            "{}",
8272            src_delta.text
8273        );
8274
8275        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8276        let sketch = expect_sketch(sketch_object);
8277        assert_eq!(sketch.constraints.len(), 1);
8278
8279        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
8280        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
8281            panic!("Expected constraint object");
8282        };
8283        let Constraint::Coincident(coincident) = constraint else {
8284            panic!("Expected coincident constraint");
8285        };
8286        let remaining_segments = vec![sketch.segments[0].into(), sketch.segments[3].into()];
8287        assert_eq!(coincident.segments, remaining_segments);
8288
8289        mock_ctx.close().await;
8290    }
8291
8292    #[tokio::test(flavor = "multi_thread")]
8293    async fn test_delete_lines_removes_multiline_equal_length_constraint_below_minimum() {
8294        let initial_source = "\
8295sketch(on = XY) {
8296  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8297  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8298  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
8299  equalLength([line1, line2, line3])
8300}
8301";
8302
8303        let program = Program::parse(initial_source).unwrap().0.unwrap();
8304
8305        let mut frontend = FrontendState::new();
8306
8307        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8308        let mock_ctx = ExecutorContext::new_mock(None).await;
8309        let version = Version(0);
8310
8311        frontend.hack_set_program(&ctx, program).await.unwrap();
8312        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8313        let sketch_id = sketch_object.id;
8314        let sketch = expect_sketch(sketch_object);
8315        let line2_id = *sketch.segments.get(5).unwrap();
8316        let line3_id = *sketch.segments.get(8).unwrap();
8317
8318        let (src_delta, scene_delta) = frontend
8319            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line2_id, line3_id])
8320            .await
8321            .unwrap();
8322        assert_eq!(
8323            src_delta.text.as_str(),
8324            "\
8325sketch(on = XY) {
8326  line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
8327}
8328"
8329        );
8330
8331        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8332        let sketch = expect_sketch(sketch_object);
8333        assert!(sketch.constraints.is_empty());
8334
8335        ctx.close().await;
8336        mock_ctx.close().await;
8337    }
8338
8339    #[tokio::test(flavor = "multi_thread")]
8340    async fn test_delete_line_preserves_multiline_parallel_constraint() {
8341        let initial_source = "\
8342sketch(on = XY) {
8343  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8344  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8345  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
8346  parallel([line1, line2, line3])
8347}
8348";
8349
8350        let program = Program::parse(initial_source).unwrap().0.unwrap();
8351
8352        let mut frontend = FrontendState::new();
8353
8354        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8355        let mock_ctx = ExecutorContext::new_mock(None).await;
8356        let version = Version(0);
8357
8358        frontend.hack_set_program(&ctx, program).await.unwrap();
8359        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8360        let sketch_id = sketch_object.id;
8361        let sketch = expect_sketch(sketch_object);
8362        let line3_id = *sketch.segments.get(8).unwrap();
8363
8364        let (src_delta, scene_delta) = frontend
8365            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line3_id])
8366            .await
8367            .unwrap();
8368        assert_eq!(
8369            src_delta.text.as_str(),
8370            "\
8371sketch(on = XY) {
8372  line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
8373  line2 = line(start = [var 5mm, var 6mm], end = [var 7mm, var 8mm])
8374  parallel([line1, line2])
8375}
8376"
8377        );
8378
8379        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8380        let sketch = expect_sketch(sketch_object);
8381        assert_eq!(sketch.constraints.len(), 1);
8382
8383        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
8384        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
8385            panic!("Expected constraint object");
8386        };
8387        let Constraint::Parallel(parallel) = constraint else {
8388            panic!("Expected parallel constraint");
8389        };
8390        assert_eq!(parallel.lines.len(), 2);
8391
8392        ctx.close().await;
8393        mock_ctx.close().await;
8394    }
8395
8396    #[tokio::test(flavor = "multi_thread")]
8397    async fn test_delete_lines_removes_multiline_parallel_constraint_below_minimum() {
8398        let initial_source = "\
8399sketch(on = XY) {
8400  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8401  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8402  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
8403  parallel([line1, line2, line3])
8404}
8405";
8406
8407        let program = Program::parse(initial_source).unwrap().0.unwrap();
8408
8409        let mut frontend = FrontendState::new();
8410
8411        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8412        let mock_ctx = ExecutorContext::new_mock(None).await;
8413        let version = Version(0);
8414
8415        frontend.hack_set_program(&ctx, program).await.unwrap();
8416        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8417        let sketch_id = sketch_object.id;
8418        let sketch = expect_sketch(sketch_object);
8419        let line2_id = *sketch.segments.get(5).unwrap();
8420        let line3_id = *sketch.segments.get(8).unwrap();
8421
8422        let (src_delta, scene_delta) = frontend
8423            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line2_id, line3_id])
8424            .await
8425            .unwrap();
8426        assert_eq!(
8427            src_delta.text.as_str(),
8428            "\
8429sketch(on = XY) {
8430  line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
8431}
8432"
8433        );
8434
8435        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8436        let sketch = expect_sketch(sketch_object);
8437        assert!(sketch.constraints.is_empty());
8438
8439        ctx.close().await;
8440        mock_ctx.close().await;
8441    }
8442
8443    #[tokio::test(flavor = "multi_thread")]
8444    async fn test_delete_line_line_coincident_constraint() {
8445        let initial_source = "\
8446sketch(on = XY) {
8447  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8448  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8449  coincident([line1, line2])
8450}
8451";
8452
8453        let program = Program::parse(initial_source).unwrap().0.unwrap();
8454
8455        let mut frontend = FrontendState::new();
8456
8457        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8458        let mock_ctx = ExecutorContext::new_mock(None).await;
8459        let version = Version(0);
8460
8461        frontend.hack_set_program(&ctx, program).await.unwrap();
8462        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8463        let sketch_id = sketch_object.id;
8464        let sketch = expect_sketch(sketch_object);
8465
8466        let coincident_id = *sketch.constraints.first().unwrap();
8467
8468        let (src_delta, scene_delta) = frontend
8469            .delete_objects(&mock_ctx, version, sketch_id, vec![coincident_id], Vec::new())
8470            .await
8471            .unwrap();
8472        assert_eq!(
8473            src_delta.text.as_str(),
8474            "\
8475sketch(on = XY) {
8476  line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
8477  line2 = line(start = [var 5mm, var 6mm], end = [var 7mm, var 8mm])
8478}
8479"
8480        );
8481        assert_eq!(scene_delta.new_objects, vec![]);
8482        assert_eq!(scene_delta.new_graph.objects.len(), 8);
8483
8484        ctx.close().await;
8485        mock_ctx.close().await;
8486    }
8487
8488    #[tokio::test(flavor = "multi_thread")]
8489    async fn test_two_points_coincident() {
8490        let initial_source = "\
8491sketch(on = XY) {
8492  point1 = point(at = [var 1, var 2])
8493  point(at = [3, 4])
8494}
8495";
8496
8497        let program = Program::parse(initial_source).unwrap().0.unwrap();
8498
8499        let mut frontend = FrontendState::new();
8500
8501        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8502        let mock_ctx = ExecutorContext::new_mock(None).await;
8503        let version = Version(0);
8504
8505        frontend.hack_set_program(&ctx, program).await.unwrap();
8506        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8507        let sketch_id = sketch_object.id;
8508        let sketch = expect_sketch(sketch_object);
8509        let point0_id = *sketch.segments.first().unwrap();
8510        let point1_id = *sketch.segments.get(1).unwrap();
8511
8512        let constraint = Constraint::Coincident(Coincident {
8513            segments: vec![point0_id.into(), point1_id.into()],
8514        });
8515        let (src_delta, scene_delta) = frontend
8516            .add_constraint(&mock_ctx, version, sketch_id, constraint)
8517            .await
8518            .unwrap();
8519        assert_eq!(
8520            src_delta.text.as_str(),
8521            "\
8522sketch(on = XY) {
8523  point1 = point(at = [var 1, var 2])
8524  point2 = point(at = [3, 4])
8525  coincident([point1, point2])
8526}
8527"
8528        );
8529        assert_eq!(
8530            scene_delta.new_graph.objects.len(),
8531            5,
8532            "{:#?}",
8533            scene_delta.new_graph.objects
8534        );
8535
8536        ctx.close().await;
8537        mock_ctx.close().await;
8538    }
8539
8540    #[tokio::test(flavor = "multi_thread")]
8541    async fn test_three_points_coincident() {
8542        let initial_source = "\
8543sketch(on = XY) {
8544  point1 = point(at = [var 1, var 2])
8545  point(at = [var 3, var 4])
8546  point(at = [var 5, var 6])
8547}
8548";
8549
8550        let program = Program::parse(initial_source).unwrap().0.unwrap();
8551
8552        let mut frontend = FrontendState::new();
8553
8554        let mock_ctx = ExecutorContext::new_mock(None).await;
8555        let version = Version(0);
8556
8557        frontend.program = program.clone();
8558        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
8559        frontend.update_state_after_exec(outcome, true);
8560        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8561        let sketch_id = sketch_object.id;
8562        let sketch = expect_sketch(sketch_object);
8563        let segments = sketch
8564            .segments
8565            .iter()
8566            .take(3)
8567            .copied()
8568            .map(Into::into)
8569            .collect::<Vec<ConstraintSegment>>();
8570
8571        let constraint = Constraint::Coincident(Coincident {
8572            segments: segments.clone(),
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 = [var 3, var 4])
8584  point3 = point(at = [var 5, var 6])
8585  coincident([point1, point2, point3])
8586}
8587"
8588        );
8589
8590        let constraint_object = scene_delta
8591            .new_graph
8592            .objects
8593            .iter()
8594            .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
8595            .unwrap();
8596
8597        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
8598            panic!("expected a constraint object");
8599        };
8600
8601        assert_eq!(constraint, &Constraint::Coincident(Coincident { segments }));
8602
8603        mock_ctx.close().await;
8604    }
8605
8606    #[tokio::test(flavor = "multi_thread")]
8607    async fn test_source_with_three_point_coincident_tracks_all_segments() {
8608        let initial_source = "\
8609sketch(on = XY) {
8610  point1 = point(at = [var 1, var 2])
8611  point2 = point(at = [var 3, var 4])
8612  point3 = point(at = [var 5, var 6])
8613  coincident([point1, point2, point3])
8614}
8615";
8616
8617        let program = Program::parse(initial_source).unwrap().0.unwrap();
8618
8619        let mut frontend = FrontendState::new();
8620
8621        let ctx = ExecutorContext::new_mock(None).await;
8622        frontend.program = program.clone();
8623        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
8624        frontend.update_state_after_exec(outcome, true);
8625
8626        let constraint_object = frontend
8627            .scene_graph
8628            .objects
8629            .iter()
8630            .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
8631            .unwrap();
8632        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
8633            panic!("expected a constraint object");
8634        };
8635
8636        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8637        let sketch = expect_sketch(sketch_object);
8638        let expected_segments = sketch
8639            .segments
8640            .iter()
8641            .take(3)
8642            .copied()
8643            .map(Into::into)
8644            .collect::<Vec<ConstraintSegment>>();
8645
8646        assert_eq!(
8647            constraint,
8648            &Constraint::Coincident(Coincident {
8649                segments: expected_segments,
8650            })
8651        );
8652
8653        ctx.close().await;
8654    }
8655
8656    #[tokio::test(flavor = "multi_thread")]
8657    async fn test_point_origin_coincident_preserves_order() {
8658        let initial_source = "\
8659sketch(on = XY) {
8660  point(at = [var 1, var 2])
8661}
8662";
8663
8664        for (origin_first, expected_source) in [
8665            (
8666                true,
8667                "\
8668sketch(on = XY) {
8669  point1 = point(at = [var 1, var 2])
8670  coincident([ORIGIN, point1])
8671}
8672",
8673            ),
8674            (
8675                false,
8676                "\
8677sketch(on = XY) {
8678  point1 = point(at = [var 1, var 2])
8679  coincident([point1, ORIGIN])
8680}
8681",
8682            ),
8683        ] {
8684            let program = Program::parse(initial_source).unwrap().0.unwrap();
8685
8686            let mut frontend = FrontendState::new();
8687
8688            let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8689            let mock_ctx = ExecutorContext::new_mock(None).await;
8690            let version = Version(0);
8691
8692            frontend.hack_set_program(&ctx, program).await.unwrap();
8693            let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8694            let sketch_id = sketch_object.id;
8695            let sketch = expect_sketch(sketch_object);
8696            let point_id = *sketch.segments.first().unwrap();
8697
8698            let segments = if origin_first {
8699                vec![ConstraintSegment::ORIGIN, point_id.into()]
8700            } else {
8701                vec![point_id.into(), ConstraintSegment::ORIGIN]
8702            };
8703            let constraint = Constraint::Coincident(Coincident {
8704                segments: segments.clone(),
8705            });
8706            let (src_delta, scene_delta) = frontend
8707                .add_constraint(&mock_ctx, version, sketch_id, constraint)
8708                .await
8709                .unwrap();
8710            assert_eq!(src_delta.text.as_str(), expected_source);
8711
8712            let constraint_object = scene_delta
8713                .new_graph
8714                .objects
8715                .iter()
8716                .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
8717                .unwrap();
8718
8719            let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
8720                panic!("expected a constraint object");
8721            };
8722
8723            assert_eq!(constraint, &Constraint::Coincident(Coincident { segments }));
8724
8725            ctx.close().await;
8726            mock_ctx.close().await;
8727        }
8728    }
8729
8730    #[tokio::test(flavor = "multi_thread")]
8731    async fn test_coincident_of_line_end_points() {
8732        let initial_source = "\
8733sketch(on = XY) {
8734  line(start = [var 1, var 2], end = [var 3, var 4])
8735  line(start = [var 5, var 6], end = [var 7, var 8])
8736}
8737";
8738
8739        let program = Program::parse(initial_source).unwrap().0.unwrap();
8740
8741        let mut frontend = FrontendState::new();
8742
8743        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8744        let mock_ctx = ExecutorContext::new_mock(None).await;
8745        let version = Version(0);
8746
8747        frontend.hack_set_program(&ctx, program).await.unwrap();
8748        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8749        let sketch_id = sketch_object.id;
8750        let sketch = expect_sketch(sketch_object);
8751        let point0_id = *sketch.segments.get(1).unwrap();
8752        let point1_id = *sketch.segments.get(3).unwrap();
8753
8754        let constraint = Constraint::Coincident(Coincident {
8755            segments: vec![point0_id.into(), point1_id.into()],
8756        });
8757        let (src_delta, scene_delta) = frontend
8758            .add_constraint(&mock_ctx, version, sketch_id, constraint)
8759            .await
8760            .unwrap();
8761        assert_eq!(
8762            src_delta.text.as_str(),
8763            "\
8764sketch(on = XY) {
8765  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8766  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8767  coincident([line1.end, line2.start])
8768}
8769"
8770        );
8771        assert_eq!(
8772            scene_delta.new_graph.objects.len(),
8773            9,
8774            "{:#?}",
8775            scene_delta.new_graph.objects
8776        );
8777
8778        ctx.close().await;
8779        mock_ctx.close().await;
8780    }
8781
8782    #[tokio::test(flavor = "multi_thread")]
8783    async fn test_coincident_of_line_point_and_circle_segment() {
8784        let initial_source = "\
8785sketch(on = XY) {
8786  circle1 = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
8787  line1 = line(start = [var 9mm, var 1mm], end = [var 10mm, var 2mm])
8788}
8789";
8790        let program = Program::parse(initial_source).unwrap().0.unwrap();
8791        let mut frontend = FrontendState::new();
8792
8793        let mock_ctx = ExecutorContext::new_mock(None).await;
8794        let version = Version(0);
8795
8796        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
8797        frontend.program = program;
8798        frontend.update_state_after_exec(outcome, true);
8799        let sketch_object = find_first_sketch_object(&frontend.scene_graph).expect("Expected sketch object");
8800        let sketch_id = sketch_object.id;
8801        let sketch = expect_sketch(sketch_object);
8802
8803        let circle_id = sketch
8804            .segments
8805            .iter()
8806            .copied()
8807            .find(|seg_id| {
8808                matches!(
8809                    &frontend.scene_graph.objects[seg_id.0].kind,
8810                    ObjectKind::Segment {
8811                        segment: Segment::Circle(_)
8812                    }
8813                )
8814            })
8815            .expect("Expected a circle segment in sketch");
8816        let line_id = sketch
8817            .segments
8818            .iter()
8819            .copied()
8820            .find(|seg_id| {
8821                matches!(
8822                    &frontend.scene_graph.objects[seg_id.0].kind,
8823                    ObjectKind::Segment {
8824                        segment: Segment::Line(_)
8825                    }
8826                )
8827            })
8828            .expect("Expected a line segment in sketch");
8829
8830        let line_start_point_id = match &frontend.scene_graph.objects[line_id.0].kind {
8831            ObjectKind::Segment {
8832                segment: Segment::Line(line),
8833            } => line.start,
8834            _ => panic!("Expected line segment object"),
8835        };
8836
8837        let constraint = Constraint::Coincident(Coincident {
8838            segments: vec![line_start_point_id.into(), circle_id.into()],
8839        });
8840        let (src_delta, _scene_delta) = frontend
8841            .add_constraint(&mock_ctx, version, sketch_id, constraint)
8842            .await
8843            .unwrap();
8844        assert_eq!(
8845            src_delta.text.as_str(),
8846            "\
8847sketch(on = XY) {
8848  circle1 = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
8849  line1 = line(start = [var 9mm, var 1mm], end = [var 10mm, var 2mm])
8850  coincident([line1.start, circle1])
8851}
8852"
8853        );
8854
8855        mock_ctx.close().await;
8856    }
8857
8858    #[tokio::test(flavor = "multi_thread")]
8859    async fn test_invalid_coincident_arc_and_line_preserves_state() {
8860        // Test that attempting an invalid coincident constraint (arc and line)
8861        // doesn't corrupt the state, allowing subsequent operations to work.
8862        // This test verifies the transactional fix in add_constraint that prevents
8863        // state corruption when invalid constraints are attempted.
8864        // Example: coincident constraint between an arc segment and a straight line segment
8865        // is geometrically invalid and should fail, but state should remain intact.
8866        // Use the programmatic approach (new_sketch + add_segment) like test_new_sketch_add_arc_edit_arc
8867        let program = Program::empty();
8868
8869        let mut frontend = FrontendState::new();
8870        frontend.program = program;
8871
8872        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8873        let mock_ctx = ExecutorContext::new_mock(None).await;
8874        let version = Version(0);
8875
8876        let sketch_args = SketchCtor {
8877            on: Plane::Default(PlaneName::Xy),
8878        };
8879        let (_src_delta, _scene_delta, sketch_id) = frontend
8880            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
8881            .await
8882            .unwrap();
8883
8884        // Add an arc segment
8885        let arc_ctor = ArcCtor {
8886            start: Point2d {
8887                x: Expr::Var(Number {
8888                    value: 0.0,
8889                    units: NumericSuffix::Mm,
8890                }),
8891                y: Expr::Var(Number {
8892                    value: 0.0,
8893                    units: NumericSuffix::Mm,
8894                }),
8895            },
8896            end: Point2d {
8897                x: Expr::Var(Number {
8898                    value: 10.0,
8899                    units: NumericSuffix::Mm,
8900                }),
8901                y: Expr::Var(Number {
8902                    value: 10.0,
8903                    units: NumericSuffix::Mm,
8904                }),
8905            },
8906            center: Point2d {
8907                x: Expr::Var(Number {
8908                    value: 10.0,
8909                    units: NumericSuffix::Mm,
8910                }),
8911                y: Expr::Var(Number {
8912                    value: 0.0,
8913                    units: NumericSuffix::Mm,
8914                }),
8915            },
8916            construction: None,
8917        };
8918        let (_src_delta, scene_delta) = frontend
8919            .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Arc(arc_ctor), None)
8920            .await
8921            .unwrap();
8922        // The arc is the last object in new_objects (after the 3 points: start, end, center)
8923        let arc_id = *scene_delta.new_objects.last().unwrap();
8924
8925        // Add a line segment
8926        let line_ctor = LineCtor {
8927            start: Point2d {
8928                x: Expr::Var(Number {
8929                    value: 20.0,
8930                    units: NumericSuffix::Mm,
8931                }),
8932                y: Expr::Var(Number {
8933                    value: 0.0,
8934                    units: NumericSuffix::Mm,
8935                }),
8936            },
8937            end: Point2d {
8938                x: Expr::Var(Number {
8939                    value: 30.0,
8940                    units: NumericSuffix::Mm,
8941                }),
8942                y: Expr::Var(Number {
8943                    value: 10.0,
8944                    units: NumericSuffix::Mm,
8945                }),
8946            },
8947            construction: None,
8948        };
8949        let (_src_delta, scene_delta) = frontend
8950            .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Line(line_ctor), None)
8951            .await
8952            .unwrap();
8953        // The line is the last object in new_objects (after the 2 points: start, end)
8954        let line_id = *scene_delta.new_objects.last().unwrap();
8955
8956        // Attempt to add an invalid coincident constraint between arc and line
8957        // This should fail during execution, but state should remain intact
8958        let constraint = Constraint::Coincident(Coincident {
8959            segments: vec![arc_id.into(), line_id.into()],
8960        });
8961        let result = frontend.add_constraint(&mock_ctx, version, sketch_id, constraint).await;
8962
8963        // The constraint addition should fail (invalid constraint)
8964        assert!(result.is_err(), "Expected invalid coincident constraint to fail");
8965
8966        // Verify state is not corrupted by checking that we can still access the scene graph
8967        // and that the original segments are still present with their source ranges
8968        let sketch_object_after =
8969            find_first_sketch_object(&frontend.scene_graph).expect("Sketch should still exist after failed constraint");
8970        let sketch_after = expect_sketch(sketch_object_after);
8971
8972        // Verify both segments are still in the sketch
8973        assert!(
8974            sketch_after.segments.contains(&arc_id),
8975            "Arc segment should still exist after failed constraint"
8976        );
8977        assert!(
8978            sketch_after.segments.contains(&line_id),
8979            "Line segment should still exist after failed constraint"
8980        );
8981
8982        // Verify we can still access segment objects (this would fail if source ranges were corrupted)
8983        let arc_obj = frontend
8984            .scene_graph
8985            .objects
8986            .get(arc_id.0)
8987            .expect("Arc object should still be accessible");
8988        let line_obj = frontend
8989            .scene_graph
8990            .objects
8991            .get(line_id.0)
8992            .expect("Line object should still be accessible");
8993
8994        // Verify source ranges are still valid (not corrupted)
8995        // Just verify that the objects are still accessible and have the expected types
8996        match &arc_obj.kind {
8997            ObjectKind::Segment {
8998                segment: Segment::Arc(_),
8999            } => {}
9000            _ => panic!("Arc object should still be an arc segment"),
9001        }
9002        match &line_obj.kind {
9003            ObjectKind::Segment {
9004                segment: Segment::Line(_),
9005            } => {}
9006            _ => panic!("Line object should still be a line segment"),
9007        }
9008
9009        ctx.close().await;
9010        mock_ctx.close().await;
9011    }
9012
9013    #[tokio::test(flavor = "multi_thread")]
9014    async fn test_distance_two_points() {
9015        let initial_source = "\
9016sketch(on = XY) {
9017  point(at = [var 1, var 2])
9018  point(at = [var 3, var 4])
9019}
9020";
9021
9022        let program = Program::parse(initial_source).unwrap().0.unwrap();
9023
9024        let mut frontend = FrontendState::new();
9025
9026        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9027        let mock_ctx = ExecutorContext::new_mock(None).await;
9028        let version = Version(0);
9029
9030        frontend.hack_set_program(&ctx, program).await.unwrap();
9031        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9032        let sketch_id = sketch_object.id;
9033        let sketch = expect_sketch(sketch_object);
9034        let point0_id = *sketch.segments.first().unwrap();
9035        let point1_id = *sketch.segments.get(1).unwrap();
9036
9037        let constraint = Constraint::Distance(Distance {
9038            points: vec![point0_id.into(), point1_id.into()],
9039            distance: Number {
9040                value: 2.0,
9041                units: NumericSuffix::Mm,
9042            },
9043            label_position: None,
9044            source: Default::default(),
9045        });
9046        let (src_delta, scene_delta) = frontend
9047            .add_constraint(&mock_ctx, version, sketch_id, constraint)
9048            .await
9049            .unwrap();
9050        assert_eq!(
9051            src_delta.text.as_str(),
9052            // The lack indentation is a formatter bug.
9053            "\
9054sketch(on = XY) {
9055  point1 = point(at = [var 1, var 2])
9056  point2 = point(at = [var 3, var 4])
9057  distance([point1, point2]) == 2mm
9058}
9059"
9060        );
9061        assert_eq!(
9062            scene_delta.new_graph.objects.len(),
9063            5,
9064            "{:#?}",
9065            scene_delta.new_graph.objects
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_with_label() {
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 mock_ctx = ExecutorContext::new_mock(None).await;
9086        let version = Version(0);
9087
9088        frontend.program = program.clone();
9089        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9090        frontend.update_state_after_exec(outcome, true);
9091        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9092        let sketch_id = sketch_object.id;
9093        let sketch = expect_sketch(sketch_object);
9094        let point0_id = *sketch.segments.first().unwrap();
9095        let point1_id = *sketch.segments.get(1).unwrap();
9096
9097        let label_position = Point2d {
9098            x: Number {
9099                value: 10.0,
9100                units: NumericSuffix::Mm,
9101            },
9102            y: Number {
9103                value: 11.0,
9104                units: NumericSuffix::Mm,
9105            },
9106        };
9107        let constraint = Constraint::Distance(Distance {
9108            points: vec![point0_id.into(), point1_id.into()],
9109            distance: Number {
9110                value: 2.0,
9111                units: NumericSuffix::Mm,
9112            },
9113            label_position: Some(label_position.clone()),
9114            source: Default::default(),
9115        });
9116        let (src_delta, scene_delta) = frontend
9117            .add_constraint(&mock_ctx, version, sketch_id, constraint)
9118            .await
9119            .unwrap();
9120        assert_eq!(
9121            src_delta.text.as_str(),
9122            "\
9123sketch(on = XY) {
9124  point1 = point(at = [var 1, var 2])
9125  point2 = point(at = [var 3, var 4])
9126  distance([point1, point2], labelPosition = [10mm, 11mm]) == 2mm
9127}
9128"
9129        );
9130
9131        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9132        let sketch = expect_sketch(sketch_object);
9133        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
9134        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9135            panic!("Expected constraint object");
9136        };
9137        let Constraint::Distance(distance) = constraint else {
9138            panic!("Expected distance constraint");
9139        };
9140        assert_eq!(distance.label_position, Some(label_position));
9141
9142        mock_ctx.close().await;
9143    }
9144
9145    #[tokio::test(flavor = "multi_thread")]
9146    async fn test_edit_distance_constraint_label_position() {
9147        let initial_source = "\
9148sketch(on = XY) {
9149  point(at = [var 1, var 2])
9150  point(at = [var 3, var 2])
9151}
9152";
9153
9154        let program = Program::parse(initial_source).unwrap().0.unwrap();
9155
9156        let mut frontend = FrontendState::new();
9157
9158        let mock_ctx = ExecutorContext::new_mock(None).await;
9159        let version = Version(0);
9160
9161        frontend.program = program.clone();
9162        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9163        frontend.update_state_after_exec(outcome, true);
9164        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9165        let sketch_id = sketch_object.id;
9166        let sketch = expect_sketch(sketch_object);
9167        let point0_id = *sketch.segments.first().unwrap();
9168        let point1_id = *sketch.segments.get(1).unwrap();
9169
9170        let constraint = Constraint::Distance(Distance {
9171            points: vec![point0_id.into(), point1_id.into()],
9172            distance: Number {
9173                value: 2.0,
9174                units: NumericSuffix::Mm,
9175            },
9176            label_position: None,
9177            source: Default::default(),
9178        });
9179        let (_, scene_delta) = frontend
9180            .add_constraint(&mock_ctx, version, sketch_id, constraint)
9181            .await
9182            .unwrap();
9183        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9184        let sketch = expect_sketch(sketch_object);
9185        let constraint_id = sketch.constraints[0];
9186        let label_position = Point2d {
9187            x: Number {
9188                value: 10.0,
9189                units: NumericSuffix::Mm,
9190            },
9191            y: Number {
9192                value: 11.0,
9193                units: NumericSuffix::Mm,
9194            },
9195        };
9196
9197        let (src_delta, scene_delta) = frontend
9198            .edit_distance_constraint_label_position(
9199                &mock_ctx,
9200                version,
9201                sketch_id,
9202                constraint_id,
9203                label_position.clone(),
9204                vec![],
9205            )
9206            .await
9207            .unwrap();
9208        assert_eq!(
9209            src_delta.text.as_str(),
9210            "\
9211sketch(on = XY) {
9212  point1 = point(at = [var 1mm, var 2mm])
9213  point2 = point(at = [var 3mm, var 2mm])
9214  distance([point1, point2], labelPosition = [10mm, 11mm]) == 2mm
9215}
9216"
9217        );
9218
9219        let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
9220        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9221            panic!("Expected constraint object");
9222        };
9223        let Constraint::Distance(distance) = constraint else {
9224            panic!("Expected distance constraint");
9225        };
9226        assert_eq!(distance.label_position, Some(label_position));
9227
9228        mock_ctx.close().await;
9229    }
9230
9231    #[tokio::test(flavor = "multi_thread")]
9232    async fn test_edit_distance_constraint_label_position_preserves_anchor_segment_solution() {
9233        let initial_source = "\
9234sketch(on = XY) {
9235  point1 = point(at = [var 0mm, var 0mm])
9236  point2 = point(at = [var 10mm, var 0mm])
9237  distance([point1, point2]) == 5mm
9238}
9239";
9240
9241        let program = Program::parse(initial_source).unwrap().0.unwrap();
9242        let mut frontend = FrontendState::new();
9243        let mock_ctx = ExecutorContext::new_mock(None).await;
9244        let version = Version(0);
9245
9246        frontend.program = program.clone();
9247        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9248        frontend.update_state_after_exec(outcome, true);
9249        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9250        let sketch_id = sketch_object.id;
9251        let sketch = expect_sketch(sketch_object);
9252        let point0_id = sketch.segments[0];
9253        let point1_id = sketch.segments[1];
9254        let constraint_id = sketch.constraints[0];
9255
9256        let edited_segments = vec![ExistingSegmentCtor {
9257            id: point0_id,
9258            ctor: SegmentCtor::Point(PointCtor {
9259                position: Point2d {
9260                    x: Expr::Var(Number {
9261                        value: 2.0,
9262                        units: NumericSuffix::Mm,
9263                    }),
9264                    y: Expr::Var(Number {
9265                        value: 1.0,
9266                        units: NumericSuffix::Mm,
9267                    }),
9268                },
9269            }),
9270        }];
9271        let (_, scene_delta) = frontend
9272            .edit_segments(&mock_ctx, version, sketch_id, edited_segments)
9273            .await
9274            .unwrap();
9275        let point0_after_segment_edit = point_position(&scene_delta.new_graph, point0_id);
9276        let point1_after_segment_edit = point_position(&scene_delta.new_graph, point1_id);
9277
9278        let label_position = Point2d {
9279            x: Number {
9280                value: 3.0,
9281                units: NumericSuffix::Mm,
9282            },
9283            y: Number {
9284                value: 4.0,
9285                units: NumericSuffix::Mm,
9286            },
9287        };
9288        let (_, scene_delta) = frontend
9289            .edit_distance_constraint_label_position(
9290                &mock_ctx,
9291                version,
9292                sketch_id,
9293                constraint_id,
9294                label_position,
9295                vec![point0_id],
9296            )
9297            .await
9298            .unwrap();
9299
9300        assert_point_position_close(
9301            point_position(&scene_delta.new_graph, point0_id),
9302            point0_after_segment_edit,
9303        );
9304        assert_point_position_close(
9305            point_position(&scene_delta.new_graph, point1_id),
9306            point1_after_segment_edit,
9307        );
9308
9309        mock_ctx.close().await;
9310    }
9311
9312    #[tokio::test(flavor = "multi_thread")]
9313    async fn test_distance_point_line() {
9314        let initial_source = "\
9315sketch(on = XY) {
9316  point(at = [var 0, var 5])
9317  line(start = [var 0, var 0], end = [var 10, var 0])
9318}
9319";
9320
9321        let program = Program::parse(initial_source).unwrap().0.unwrap();
9322
9323        let mut frontend = FrontendState::new();
9324
9325        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9326        let mock_ctx = ExecutorContext::new_mock(None).await;
9327        let version = Version(0);
9328
9329        frontend.hack_set_program(&ctx, program).await.unwrap();
9330        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9331        let sketch_id = sketch_object.id;
9332        let sketch = expect_sketch(sketch_object);
9333        let point_id = *sketch.segments.first().unwrap();
9334        let line_id = *sketch
9335            .segments
9336            .iter()
9337            .find(|segment_id| {
9338                matches!(
9339                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
9340                    Some(ObjectKind::Segment {
9341                        segment: Segment::Line(_)
9342                    })
9343                )
9344            })
9345            .unwrap();
9346
9347        let label_position = Point2d {
9348            x: Number {
9349                value: 10.0,
9350                units: NumericSuffix::Mm,
9351            },
9352            y: Number {
9353                value: 11.0,
9354                units: NumericSuffix::Mm,
9355            },
9356        };
9357        let constraint = Constraint::Distance(Distance {
9358            points: vec![point_id.into(), line_id.into()],
9359            distance: Number {
9360                value: 5.0,
9361                units: NumericSuffix::Mm,
9362            },
9363            label_position: Some(label_position.clone()),
9364            source: Default::default(),
9365        });
9366        let (src_delta, scene_delta) = frontend
9367            .add_constraint(&mock_ctx, version, sketch_id, constraint)
9368            .await
9369            .unwrap();
9370        assert_eq!(
9371            src_delta.text.as_str(),
9372            "\
9373sketch(on = XY) {
9374  point1 = point(at = [var 0, var 5])
9375  line1 = line(start = [var 0, var 0], end = [var 10, var 0])
9376  distance([point1, line1], labelPosition = [10mm, 11mm]) == 5mm
9377}
9378"
9379        );
9380        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9381        let sketch = expect_sketch(sketch_object);
9382        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
9383        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9384            panic!("Expected constraint object");
9385        };
9386        let Constraint::Distance(distance) = constraint else {
9387            panic!("Expected distance constraint");
9388        };
9389        assert_eq!(distance.label_position, Some(label_position));
9390
9391        ctx.close().await;
9392        mock_ctx.close().await;
9393    }
9394
9395    #[tokio::test(flavor = "multi_thread")]
9396    async fn test_distance_point_arc() {
9397        let initial_source = "\
9398sketch(on = XY) {
9399  point(at = [var 0, var 8])
9400  arc(start = [var 5, var 0], end = [var 0, var 5], center = [var 0, var 0])
9401}
9402";
9403
9404        let program = Program::parse(initial_source).unwrap().0.unwrap();
9405
9406        let mut frontend = FrontendState::new();
9407
9408        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9409        let mock_ctx = ExecutorContext::new_mock(None).await;
9410        let version = Version(0);
9411
9412        frontend.hack_set_program(&ctx, program).await.unwrap();
9413        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9414        let sketch_id = sketch_object.id;
9415        let sketch = expect_sketch(sketch_object);
9416        let point_id = *sketch.segments.first().unwrap();
9417        let arc_id = *sketch
9418            .segments
9419            .iter()
9420            .find(|segment_id| {
9421                matches!(
9422                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
9423                    Some(ObjectKind::Segment {
9424                        segment: Segment::Arc(_)
9425                    })
9426                )
9427            })
9428            .unwrap();
9429
9430        let constraint = Constraint::Distance(Distance {
9431            points: vec![point_id.into(), arc_id.into()],
9432            distance: Number {
9433                value: 3.0,
9434                units: NumericSuffix::Mm,
9435            },
9436            label_position: None,
9437            source: Default::default(),
9438        });
9439        let (src_delta, _scene_delta) = frontend
9440            .add_constraint(&mock_ctx, version, sketch_id, constraint)
9441            .await
9442            .unwrap();
9443        assert_eq!(
9444            src_delta.text.as_str(),
9445            "\
9446sketch(on = XY) {
9447  point1 = point(at = [var 0, var 8])
9448  arc1 = arc(start = [var 5, var 0], end = [var 0, var 5], center = [var 0, var 0])
9449  distance([point1, arc1]) == 3mm
9450}
9451"
9452        );
9453
9454        ctx.close().await;
9455        mock_ctx.close().await;
9456    }
9457
9458    #[tokio::test(flavor = "multi_thread")]
9459    async fn test_distance_arc_origin() {
9460        let initial_source = "\
9461sketch001 = sketch(on = XY) {
9462  arc(start = [var -4.13mm, var -0.59mm], end = [var -3.47mm, var 3.38mm], center = [var -4.55mm, var 1.52mm])
9463}
9464";
9465
9466        let program = Program::parse(initial_source).unwrap().0.unwrap();
9467
9468        let mut frontend = FrontendState::new();
9469
9470        let mock_ctx = ExecutorContext::new_mock(None).await;
9471        let version = Version(0);
9472
9473        frontend.program = program.clone();
9474        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9475        frontend.update_state_after_exec(outcome, true);
9476        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9477        let sketch_id = sketch_object.id;
9478        let sketch = expect_sketch(sketch_object);
9479        let arc_id = *sketch
9480            .segments
9481            .iter()
9482            .find(|segment_id| {
9483                matches!(
9484                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
9485                    Some(ObjectKind::Segment {
9486                        segment: Segment::Arc(_)
9487                    })
9488                )
9489            })
9490            .unwrap();
9491
9492        let constraint = Constraint::Distance(Distance {
9493            points: vec![arc_id.into(), ConstraintSegment::ORIGIN],
9494            distance: Number {
9495                value: 3.0,
9496                units: NumericSuffix::Mm,
9497            },
9498            label_position: None,
9499            source: Default::default(),
9500        });
9501        let (src_delta, _scene_delta) = frontend
9502            .add_constraint(&mock_ctx, version, sketch_id, constraint)
9503            .await
9504            .unwrap();
9505        assert_eq!(
9506            src_delta.text.as_str(),
9507            "\
9508sketch001 = sketch(on = XY) {
9509  arc1 = arc(start = [var -4.13mm, var -0.59mm], end = [var -3.47mm, var 3.38mm], center = [var -4.55mm, var 1.52mm])
9510  distance([arc1, ORIGIN]) == 3mm
9511}
9512"
9513        );
9514
9515        mock_ctx.close().await;
9516    }
9517
9518    #[tokio::test(flavor = "multi_thread")]
9519    async fn test_distance_line_origin() {
9520        let initial_source = "\
9521sketch(on = XY) {
9522  line(start = [var 5, var 0], end = [var 5, var 10])
9523}
9524";
9525
9526        let program = Program::parse(initial_source).unwrap().0.unwrap();
9527
9528        let mut frontend = FrontendState::new();
9529
9530        let mock_ctx = ExecutorContext::new_mock(None).await;
9531        let version = Version(0);
9532
9533        frontend.program = program.clone();
9534        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9535        frontend.update_state_after_exec(outcome, true);
9536        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9537        let sketch_id = sketch_object.id;
9538        let sketch = expect_sketch(sketch_object);
9539        let line_id = *sketch
9540            .segments
9541            .iter()
9542            .find(|segment_id| {
9543                matches!(
9544                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
9545                    Some(ObjectKind::Segment {
9546                        segment: Segment::Line(_)
9547                    })
9548                )
9549            })
9550            .unwrap();
9551
9552        let constraint = Constraint::Distance(Distance {
9553            points: vec![ConstraintSegment::ORIGIN, line_id.into()],
9554            distance: Number {
9555                value: 5.0,
9556                units: NumericSuffix::Mm,
9557            },
9558            label_position: None,
9559            source: Default::default(),
9560        });
9561        let (src_delta, _scene_delta) = frontend
9562            .add_constraint(&mock_ctx, version, sketch_id, constraint)
9563            .await
9564            .unwrap();
9565        assert_eq!(
9566            src_delta.text.as_str(),
9567            "\
9568sketch(on = XY) {
9569  line1 = line(start = [var 5, var 0], end = [var 5, var 10])
9570  distance([ORIGIN, line1]) == 5mm
9571}
9572"
9573        );
9574
9575        mock_ctx.close().await;
9576    }
9577
9578    #[tokio::test(flavor = "multi_thread")]
9579    async fn test_distance_line_circle() {
9580        let initial_source = "\
9581sketch(on = XY) {
9582  line(start = [var -10, var 8], end = [var 10, var 8])
9583  circle(start = [var 5, var 0], center = [var 0, var 0])
9584}
9585";
9586
9587        let program = Program::parse(initial_source).unwrap().0.unwrap();
9588
9589        let mut frontend = FrontendState::new();
9590
9591        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9592        let mock_ctx = ExecutorContext::new_mock(None).await;
9593        let version = Version(0);
9594
9595        frontend.hack_set_program(&ctx, program).await.unwrap();
9596        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9597        let sketch_id = sketch_object.id;
9598        let sketch = expect_sketch(sketch_object);
9599        let line_id = *sketch
9600            .segments
9601            .iter()
9602            .find(|segment_id| {
9603                matches!(
9604                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
9605                    Some(ObjectKind::Segment {
9606                        segment: Segment::Line(_)
9607                    })
9608                )
9609            })
9610            .unwrap();
9611        let circle_id = *sketch
9612            .segments
9613            .iter()
9614            .find(|segment_id| {
9615                matches!(
9616                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
9617                    Some(ObjectKind::Segment {
9618                        segment: Segment::Circle(_)
9619                    })
9620                )
9621            })
9622            .unwrap();
9623
9624        let constraint = Constraint::Distance(Distance {
9625            points: vec![line_id.into(), circle_id.into()],
9626            distance: Number {
9627                value: 3.0,
9628                units: NumericSuffix::Mm,
9629            },
9630            label_position: None,
9631            source: Default::default(),
9632        });
9633        let (src_delta, _scene_delta) = frontend
9634            .add_constraint(&mock_ctx, version, sketch_id, constraint)
9635            .await
9636            .unwrap();
9637        assert_eq!(
9638            src_delta.text.as_str(),
9639            "\
9640sketch(on = XY) {
9641  line1 = line(start = [var -10, var 8], end = [var 10, var 8])
9642  circle1 = circle(start = [var 5, var 0], center = [var 0, var 0])
9643  distance([line1, circle1]) == 3mm
9644}
9645"
9646        );
9647
9648        ctx.close().await;
9649        mock_ctx.close().await;
9650    }
9651
9652    #[tokio::test(flavor = "multi_thread")]
9653    async fn test_distance_circle_arc() {
9654        let initial_source = "\
9655sketch(on = XY) {
9656  circle(start = [var 5, var 0], center = [var 0, var 0])
9657  arc(start = [var 15, var 0], end = [var 10, var 5], center = [var 10, var 0])
9658}
9659";
9660
9661        let program = Program::parse(initial_source).unwrap().0.unwrap();
9662
9663        let mut frontend = FrontendState::new();
9664
9665        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9666        let mock_ctx = ExecutorContext::new_mock(None).await;
9667        let version = Version(0);
9668
9669        frontend.hack_set_program(&ctx, program).await.unwrap();
9670        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9671        let sketch_id = sketch_object.id;
9672        let sketch = expect_sketch(sketch_object);
9673        let circle_id = *sketch
9674            .segments
9675            .iter()
9676            .find(|segment_id| {
9677                matches!(
9678                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
9679                    Some(ObjectKind::Segment {
9680                        segment: Segment::Circle(_)
9681                    })
9682                )
9683            })
9684            .unwrap();
9685        let arc_id = *sketch
9686            .segments
9687            .iter()
9688            .find(|segment_id| {
9689                matches!(
9690                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
9691                    Some(ObjectKind::Segment {
9692                        segment: Segment::Arc(_)
9693                    })
9694                )
9695            })
9696            .unwrap();
9697
9698        let constraint = Constraint::Distance(Distance {
9699            points: vec![circle_id.into(), arc_id.into()],
9700            distance: Number {
9701                value: 3.0,
9702                units: NumericSuffix::Mm,
9703            },
9704            label_position: None,
9705            source: Default::default(),
9706        });
9707        let (src_delta, _scene_delta) = frontend
9708            .add_constraint(&mock_ctx, version, sketch_id, constraint)
9709            .await
9710            .unwrap();
9711        assert_eq!(
9712            src_delta.text.as_str(),
9713            "\
9714sketch(on = XY) {
9715  circle1 = circle(start = [var 5, var 0], center = [var 0, var 0])
9716  arc1 = arc(start = [var 15, var 0], end = [var 10, var 5], center = [var 10, var 0])
9717  distance([circle1, arc1]) == 3mm
9718}
9719"
9720        );
9721
9722        ctx.close().await;
9723        mock_ctx.close().await;
9724    }
9725
9726    #[tokio::test(flavor = "multi_thread")]
9727    async fn test_distance_parallel_lines() {
9728        let initial_source = "\
9729sketch(on = XY) {
9730  line(start = [var 0, var 0], end = [var 10, var 0])
9731  line(start = [var 0, var 5], end = [var 10, var 5])
9732}
9733";
9734
9735        let program = Program::parse(initial_source).unwrap().0.unwrap();
9736
9737        let mut frontend = FrontendState::new();
9738
9739        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9740        let mock_ctx = ExecutorContext::new_mock(None).await;
9741        let version = Version(0);
9742
9743        frontend.hack_set_program(&ctx, program).await.unwrap();
9744        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9745        let sketch_id = sketch_object.id;
9746        let sketch = expect_sketch(sketch_object);
9747        let line_ids = sketch
9748            .segments
9749            .iter()
9750            .copied()
9751            .filter(|segment_id| {
9752                matches!(
9753                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
9754                    Some(ObjectKind::Segment {
9755                        segment: Segment::Line(_)
9756                    })
9757                )
9758            })
9759            .collect::<Vec<_>>();
9760
9761        let constraint = Constraint::Distance(Distance {
9762            points: vec![line_ids[0].into(), line_ids[1].into()],
9763            distance: Number {
9764                value: 5.0,
9765                units: NumericSuffix::Mm,
9766            },
9767            label_position: None,
9768            source: Default::default(),
9769        });
9770        let (src_delta, _scene_delta) = frontend
9771            .add_constraint(&mock_ctx, version, sketch_id, constraint)
9772            .await
9773            .unwrap();
9774        assert_eq!(
9775            src_delta.text.as_str(),
9776            "\
9777sketch(on = XY) {
9778  line1 = line(start = [var 0, var 0], end = [var 10, var 0])
9779  line2 = line(start = [var 0, var 5], end = [var 10, var 5])
9780  distance([line1, line2]) == 5mm
9781}
9782"
9783        );
9784
9785        ctx.close().await;
9786        mock_ctx.close().await;
9787    }
9788
9789    #[tokio::test(flavor = "multi_thread")]
9790    async fn test_distance_non_parallel_lines_lowers_to_distance() {
9791        let initial_source = "\
9792sketch(on = XY) {
9793  line(start = [var 0, var 0], end = [var 10, var 0])
9794  line(start = [var 0, var 0], end = [var 0, var 10])
9795}
9796";
9797
9798        let program = Program::parse(initial_source).unwrap().0.unwrap();
9799
9800        let mut frontend = FrontendState::new();
9801
9802        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9803        let mock_ctx = ExecutorContext::new_mock(None).await;
9804        let version = Version(0);
9805
9806        frontend.hack_set_program(&ctx, program).await.unwrap();
9807        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9808        let sketch_id = sketch_object.id;
9809        let sketch = expect_sketch(sketch_object);
9810        let line_ids = sketch
9811            .segments
9812            .iter()
9813            .copied()
9814            .filter(|segment_id| {
9815                matches!(
9816                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
9817                    Some(ObjectKind::Segment {
9818                        segment: Segment::Line(_)
9819                    })
9820                )
9821            })
9822            .collect::<Vec<_>>();
9823
9824        let constraint = Constraint::Distance(Distance {
9825            points: vec![line_ids[0].into(), line_ids[1].into()],
9826            distance: Number {
9827                value: 5.0,
9828                units: NumericSuffix::Mm,
9829            },
9830            label_position: None,
9831            source: Default::default(),
9832        });
9833        let (src_delta, _scene_delta) = frontend
9834            .add_constraint(&mock_ctx, version, sketch_id, constraint)
9835            .await
9836            .unwrap();
9837        assert_eq!(
9838            src_delta.text.as_str(),
9839            "\
9840sketch(on = XY) {
9841  line1 = line(start = [var 0, var 0], end = [var 10, var 0])
9842  line2 = line(start = [var 0, var 0], end = [var 0, var 10])
9843  distance([line1, line2]) == 5mm
9844}
9845"
9846        );
9847
9848        ctx.close().await;
9849        mock_ctx.close().await;
9850    }
9851
9852    #[tokio::test(flavor = "multi_thread")]
9853    async fn test_horizontal_distance_two_points() {
9854        let initial_source = "\
9855sketch(on = XY) {
9856  point(at = [var 1, var 2])
9857  point(at = [var 3, var 4])
9858}
9859";
9860
9861        let program = Program::parse(initial_source).unwrap().0.unwrap();
9862
9863        let mut frontend = FrontendState::new();
9864
9865        let mock_ctx = ExecutorContext::new_mock(None).await;
9866        let version = Version(0);
9867
9868        frontend.program = program.clone();
9869        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9870        frontend.update_state_after_exec(outcome, true);
9871        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9872        let sketch_id = sketch_object.id;
9873        let sketch = expect_sketch(sketch_object);
9874        let point0_id = *sketch.segments.first().unwrap();
9875        let point1_id = *sketch.segments.get(1).unwrap();
9876        let label_position = Point2d {
9877            x: Number {
9878                value: 10.0,
9879                units: NumericSuffix::Mm,
9880            },
9881            y: Number {
9882                value: 11.0,
9883                units: NumericSuffix::Mm,
9884            },
9885        };
9886
9887        let constraint = Constraint::HorizontalDistance(Distance {
9888            points: vec![point0_id.into(), point1_id.into()],
9889            distance: Number {
9890                value: 2.0,
9891                units: NumericSuffix::Mm,
9892            },
9893            label_position: Some(label_position.clone()),
9894            source: Default::default(),
9895        });
9896        let (src_delta, scene_delta) = frontend
9897            .add_constraint(&mock_ctx, version, sketch_id, constraint)
9898            .await
9899            .unwrap();
9900        assert_eq!(
9901            src_delta.text.as_str(),
9902            // The lack indentation is a formatter bug.
9903            "\
9904sketch(on = XY) {
9905  point1 = point(at = [var 1, var 2])
9906  point2 = point(at = [var 3, var 4])
9907  horizontalDistance([point1, point2], labelPosition = [10mm, 11mm]) == 2mm
9908}
9909"
9910        );
9911        assert_eq!(
9912            scene_delta.new_graph.objects.len(),
9913            5,
9914            "{:#?}",
9915            scene_delta.new_graph.objects
9916        );
9917        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9918        let sketch = expect_sketch(sketch_object);
9919        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
9920        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9921            panic!("Expected constraint object");
9922        };
9923        let Constraint::HorizontalDistance(distance) = constraint else {
9924            panic!("Expected horizontal distance constraint");
9925        };
9926        assert_eq!(distance.label_position, Some(label_position));
9927
9928        mock_ctx.close().await;
9929    }
9930
9931    #[tokio::test(flavor = "multi_thread")]
9932    async fn test_radius_single_arc_segment() {
9933        let initial_source = "\
9934sketch(on = XY) {
9935  arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
9936}
9937";
9938
9939        let program = Program::parse(initial_source).unwrap().0.unwrap();
9940
9941        let mut frontend = FrontendState::new();
9942
9943        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9944        let mock_ctx = ExecutorContext::new_mock(None).await;
9945        let version = Version(0);
9946
9947        frontend.hack_set_program(&ctx, program).await.unwrap();
9948        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9949        let sketch_id = sketch_object.id;
9950        let sketch = expect_sketch(sketch_object);
9951        // Find the arc segment (not the points)
9952        let arc_id = sketch
9953            .segments
9954            .iter()
9955            .find(|&seg_id| {
9956                let obj = frontend.scene_graph.objects.get(seg_id.0);
9957                matches!(
9958                    obj.map(|o| &o.kind),
9959                    Some(ObjectKind::Segment {
9960                        segment: Segment::Arc(_)
9961                    })
9962                )
9963            })
9964            .unwrap();
9965
9966        let constraint = Constraint::Radius(Radius {
9967            arc: *arc_id,
9968            radius: Number {
9969                value: 5.0,
9970                units: NumericSuffix::Mm,
9971            },
9972            label_position: None,
9973            source: Default::default(),
9974        });
9975        let (src_delta, scene_delta) = frontend
9976            .add_constraint(&mock_ctx, version, sketch_id, constraint)
9977            .await
9978            .unwrap();
9979        assert_eq!(
9980            src_delta.text.as_str(),
9981            // The lack indentation is a formatter bug.
9982            "\
9983sketch(on = XY) {
9984  arc1 = arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
9985  radius(arc1) == 5mm
9986}
9987"
9988        );
9989        assert_eq!(
9990            scene_delta.new_graph.objects.len(),
9991            7, // Plane (0) + Sketch (1) + Start point (2) + End point (3) + Center point (4) + Arc (5) + Constraint (6)
9992            "{:#?}",
9993            scene_delta.new_graph.objects
9994        );
9995
9996        ctx.close().await;
9997        mock_ctx.close().await;
9998    }
9999
10000    #[tokio::test(flavor = "multi_thread")]
10001    async fn test_radius_single_arc_segment_with_label_position() {
10002        let initial_source = "\
10003sketch(on = XY) {
10004  arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
10005}
10006";
10007
10008        let program = Program::parse(initial_source).unwrap().0.unwrap();
10009        let mut frontend = FrontendState::new();
10010        let mock_ctx = ExecutorContext::new_mock(None).await;
10011        let version = Version(0);
10012
10013        frontend.program = program.clone();
10014        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10015        frontend.update_state_after_exec(outcome, true);
10016        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10017        let sketch_id = sketch_object.id;
10018        let sketch = expect_sketch(sketch_object);
10019        let arc_id = sketch
10020            .segments
10021            .iter()
10022            .find(|&seg_id| {
10023                let obj = frontend.scene_graph.objects.get(seg_id.0);
10024                matches!(
10025                    obj.map(|o| &o.kind),
10026                    Some(ObjectKind::Segment {
10027                        segment: Segment::Arc(_)
10028                    })
10029                )
10030            })
10031            .unwrap();
10032
10033        let label_position = Point2d {
10034            x: Number {
10035                value: 10.0,
10036                units: NumericSuffix::Mm,
10037            },
10038            y: Number {
10039                value: 11.0,
10040                units: NumericSuffix::Mm,
10041            },
10042        };
10043        let constraint = Constraint::Radius(Radius {
10044            arc: *arc_id,
10045            radius: Number {
10046                value: 5.0,
10047                units: NumericSuffix::Mm,
10048            },
10049            label_position: Some(label_position.clone()),
10050            source: Default::default(),
10051        });
10052        let (src_delta, scene_delta) = frontend
10053            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10054            .await
10055            .unwrap();
10056        assert_eq!(
10057            src_delta.text.as_str(),
10058            "\
10059sketch(on = XY) {
10060  arc1 = arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
10061  radius(arc1, labelPosition = [10mm, 11mm]) == 5mm
10062}
10063"
10064        );
10065
10066        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10067        let sketch = expect_sketch(sketch_object);
10068        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10069        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10070            panic!("Expected constraint object");
10071        };
10072        let Constraint::Radius(radius) = constraint else {
10073            panic!("Expected radius constraint");
10074        };
10075        assert_eq!(radius.label_position, Some(label_position));
10076
10077        mock_ctx.close().await;
10078    }
10079
10080    #[tokio::test(flavor = "multi_thread")]
10081    async fn test_edit_radius_constraint_label_position() {
10082        let initial_source = "\
10083sketch(on = XY) {
10084  arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
10085  radius(arc1) == 5mm
10086}
10087";
10088
10089        let program = Program::parse(initial_source).unwrap().0.unwrap();
10090        let mut frontend = FrontendState::new();
10091        let mock_ctx = ExecutorContext::new_mock(None).await;
10092        let version = Version(0);
10093
10094        frontend.program = program.clone();
10095        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10096        frontend.update_state_after_exec(outcome, true);
10097        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10098        let sketch_id = sketch_object.id;
10099        let sketch = expect_sketch(sketch_object);
10100        let constraint_id = sketch.constraints[0];
10101        let label_position = Point2d {
10102            x: Number {
10103                value: 10.0,
10104                units: NumericSuffix::Mm,
10105            },
10106            y: Number {
10107                value: 11.0,
10108                units: NumericSuffix::Mm,
10109            },
10110        };
10111
10112        let (src_delta, scene_delta) = frontend
10113            .edit_distance_constraint_label_position(
10114                &mock_ctx,
10115                version,
10116                sketch_id,
10117                constraint_id,
10118                label_position.clone(),
10119                vec![],
10120            )
10121            .await
10122            .unwrap();
10123        assert_eq!(
10124            src_delta.text.as_str(),
10125            "\
10126sketch(on = XY) {
10127  arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
10128  radius(arc1, labelPosition = [10mm, 11mm]) == 5mm
10129}
10130"
10131        );
10132
10133        let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
10134        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10135            panic!("Expected constraint object");
10136        };
10137        let Constraint::Radius(radius) = constraint else {
10138            panic!("Expected radius constraint");
10139        };
10140        assert_eq!(radius.label_position, Some(label_position));
10141
10142        mock_ctx.close().await;
10143    }
10144
10145    #[tokio::test(flavor = "multi_thread")]
10146    async fn test_vertical_distance_two_points() {
10147        let initial_source = "\
10148sketch(on = XY) {
10149  point(at = [var 1, var 2])
10150  point(at = [var 3, var 4])
10151}
10152";
10153
10154        let program = Program::parse(initial_source).unwrap().0.unwrap();
10155
10156        let mut frontend = FrontendState::new();
10157
10158        let mock_ctx = ExecutorContext::new_mock(None).await;
10159        let version = Version(0);
10160
10161        frontend.program = program.clone();
10162        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10163        frontend.update_state_after_exec(outcome, true);
10164        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10165        let sketch_id = sketch_object.id;
10166        let sketch = expect_sketch(sketch_object);
10167        let point0_id = *sketch.segments.first().unwrap();
10168        let point1_id = *sketch.segments.get(1).unwrap();
10169        let label_position = Point2d {
10170            x: Number {
10171                value: 10.0,
10172                units: NumericSuffix::Mm,
10173            },
10174            y: Number {
10175                value: 11.0,
10176                units: NumericSuffix::Mm,
10177            },
10178        };
10179
10180        let constraint = Constraint::VerticalDistance(Distance {
10181            points: vec![point0_id.into(), point1_id.into()],
10182            distance: Number {
10183                value: 2.0,
10184                units: NumericSuffix::Mm,
10185            },
10186            label_position: Some(label_position.clone()),
10187            source: Default::default(),
10188        });
10189        let (src_delta, scene_delta) = frontend
10190            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10191            .await
10192            .unwrap();
10193        assert_eq!(
10194            src_delta.text.as_str(),
10195            // The lack indentation is a formatter bug.
10196            "\
10197sketch(on = XY) {
10198  point1 = point(at = [var 1, var 2])
10199  point2 = point(at = [var 3, var 4])
10200  verticalDistance([point1, point2], labelPosition = [10mm, 11mm]) == 2mm
10201}
10202"
10203        );
10204        assert_eq!(
10205            scene_delta.new_graph.objects.len(),
10206            5,
10207            "{:#?}",
10208            scene_delta.new_graph.objects
10209        );
10210        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10211        let sketch = expect_sketch(sketch_object);
10212        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10213        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10214            panic!("Expected constraint object");
10215        };
10216        let Constraint::VerticalDistance(distance) = constraint else {
10217            panic!("Expected vertical distance constraint");
10218        };
10219        assert_eq!(distance.label_position, Some(label_position));
10220
10221        mock_ctx.close().await;
10222    }
10223
10224    #[tokio::test(flavor = "multi_thread")]
10225    async fn test_add_fixed_standalone_point() {
10226        let initial_source = "\
10227sketch(on = XY) {
10228  point(at = [var 1, var 2])
10229}
10230";
10231
10232        let program = Program::parse(initial_source).unwrap().0.unwrap();
10233
10234        let mut frontend = FrontendState::new();
10235
10236        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10237        let mock_ctx = ExecutorContext::new_mock(None).await;
10238        let version = Version(0);
10239
10240        frontend.hack_set_program(&ctx, program).await.unwrap();
10241        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10242        let sketch_id = sketch_object.id;
10243        let sketch = expect_sketch(sketch_object);
10244        let point_id = *sketch.segments.first().unwrap();
10245
10246        let (src_delta, scene_delta) = frontend
10247            .add_constraint(
10248                &mock_ctx,
10249                version,
10250                sketch_id,
10251                Constraint::Fixed(Fixed {
10252                    points: vec![FixedPoint {
10253                        point: point_id,
10254                        position: Point2d {
10255                            x: Number {
10256                                value: 2.0,
10257                                units: NumericSuffix::Mm,
10258                            },
10259                            y: Number {
10260                                value: 3.0,
10261                                units: NumericSuffix::Mm,
10262                            },
10263                        },
10264                    }],
10265                }),
10266            )
10267            .await
10268            .unwrap();
10269        assert_eq!(
10270            src_delta.text.as_str(),
10271            "\
10272sketch(on = XY) {
10273  point1 = point(at = [var 1, var 2])
10274  fixed([point1, [2mm, 3mm]])
10275}
10276"
10277        );
10278        assert_eq!(
10279            scene_delta.new_graph.objects.len(),
10280            4,
10281            "{:#?}",
10282            scene_delta.new_graph.objects
10283        );
10284
10285        ctx.close().await;
10286        mock_ctx.close().await;
10287    }
10288
10289    #[tokio::test(flavor = "multi_thread")]
10290    async fn test_add_fixed_multiple_points() {
10291        let initial_source = "\
10292sketch(on = XY) {
10293  point(at = [var 1, var 2])
10294  point(at = [var 3, var 4])
10295}
10296";
10297
10298        let program = Program::parse(initial_source).unwrap().0.unwrap();
10299
10300        let mut frontend = FrontendState::new();
10301
10302        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10303        let mock_ctx = ExecutorContext::new_mock(None).await;
10304        let version = Version(0);
10305
10306        frontend.hack_set_program(&ctx, program).await.unwrap();
10307        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10308        let sketch_id = sketch_object.id;
10309        let sketch = expect_sketch(sketch_object);
10310        let point0_id = *sketch.segments.first().unwrap();
10311        let point1_id = *sketch.segments.get(1).unwrap();
10312
10313        let (src_delta, scene_delta) = frontend
10314            .add_constraint(
10315                &mock_ctx,
10316                version,
10317                sketch_id,
10318                Constraint::Fixed(Fixed {
10319                    points: vec![
10320                        FixedPoint {
10321                            point: point0_id,
10322                            position: Point2d {
10323                                x: Number {
10324                                    value: 2.0,
10325                                    units: NumericSuffix::Mm,
10326                                },
10327                                y: Number {
10328                                    value: 3.0,
10329                                    units: NumericSuffix::Mm,
10330                                },
10331                            },
10332                        },
10333                        FixedPoint {
10334                            point: point1_id,
10335                            position: Point2d {
10336                                x: Number {
10337                                    value: 4.0,
10338                                    units: NumericSuffix::Mm,
10339                                },
10340                                y: Number {
10341                                    value: 5.0,
10342                                    units: NumericSuffix::Mm,
10343                                },
10344                            },
10345                        },
10346                    ],
10347                }),
10348            )
10349            .await
10350            .unwrap();
10351        assert_eq!(
10352            src_delta.text.as_str(),
10353            "\
10354sketch(on = XY) {
10355  point1 = point(at = [var 1, var 2])
10356  point2 = point(at = [var 3, var 4])
10357  fixed([point1, [2mm, 3mm]])
10358  fixed([point2, [4mm, 5mm]])
10359}
10360"
10361        );
10362        assert_eq!(
10363            scene_delta.new_graph.objects.len(),
10364            6,
10365            "{:#?}",
10366            scene_delta.new_graph.objects
10367        );
10368
10369        ctx.close().await;
10370        mock_ctx.close().await;
10371    }
10372
10373    #[tokio::test(flavor = "multi_thread")]
10374    async fn test_add_fixed_owned_point() {
10375        let initial_source = "\
10376sketch(on = XY) {
10377  line(start = [var 1, var 2], end = [var 3, var 4])
10378}
10379";
10380
10381        let program = Program::parse(initial_source).unwrap().0.unwrap();
10382
10383        let mut frontend = FrontendState::new();
10384
10385        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10386        let mock_ctx = ExecutorContext::new_mock(None).await;
10387        let version = Version(0);
10388
10389        frontend.hack_set_program(&ctx, program).await.unwrap();
10390        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10391        let sketch_id = sketch_object.id;
10392        let sketch = expect_sketch(sketch_object);
10393        let line_start_id = *sketch.segments.first().unwrap();
10394
10395        let (src_delta, scene_delta) = frontend
10396            .add_constraint(
10397                &mock_ctx,
10398                version,
10399                sketch_id,
10400                Constraint::Fixed(Fixed {
10401                    points: vec![FixedPoint {
10402                        point: line_start_id,
10403                        position: Point2d {
10404                            x: Number {
10405                                value: 2.0,
10406                                units: NumericSuffix::Mm,
10407                            },
10408                            y: Number {
10409                                value: 3.0,
10410                                units: NumericSuffix::Mm,
10411                            },
10412                        },
10413                    }],
10414                }),
10415            )
10416            .await
10417            .unwrap();
10418        assert_eq!(
10419            src_delta.text.as_str(),
10420            "\
10421sketch(on = XY) {
10422  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10423  fixed([line1.start, [2mm, 3mm]])
10424}
10425"
10426        );
10427        assert_eq!(
10428            scene_delta.new_graph.objects.len(),
10429            6,
10430            "{:#?}",
10431            scene_delta.new_graph.objects
10432        );
10433
10434        ctx.close().await;
10435        mock_ctx.close().await;
10436    }
10437
10438    #[tokio::test(flavor = "multi_thread")]
10439    async fn test_radius_error_cases() {
10440        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10441        let mock_ctx = ExecutorContext::new_mock(None).await;
10442        let version = Version(0);
10443
10444        // Test: Single point should error
10445        let initial_source_point = "\
10446sketch(on = XY) {
10447  point(at = [var 1, var 2])
10448}
10449";
10450        let program_point = Program::parse(initial_source_point).unwrap().0.unwrap();
10451        let mut frontend_point = FrontendState::new();
10452        frontend_point.hack_set_program(&ctx, program_point).await.unwrap();
10453        let sketch_object_point = find_first_sketch_object(&frontend_point.scene_graph).unwrap();
10454        let sketch_id_point = sketch_object_point.id;
10455        let sketch_point = expect_sketch(sketch_object_point);
10456        let point_id = *sketch_point.segments.first().unwrap();
10457
10458        let constraint_point = Constraint::Radius(Radius {
10459            arc: point_id,
10460            radius: Number {
10461                value: 5.0,
10462                units: NumericSuffix::Mm,
10463            },
10464            label_position: None,
10465            source: Default::default(),
10466        });
10467        let result_point = frontend_point
10468            .add_constraint(&mock_ctx, version, sketch_id_point, constraint_point)
10469            .await;
10470        assert!(result_point.is_err(), "Single point should error for radius");
10471
10472        // Test: Single line segment should error (only arc segments supported)
10473        let initial_source_line = "\
10474sketch(on = XY) {
10475  line(start = [var 1, var 2], end = [var 3, var 4])
10476}
10477";
10478        let program_line = Program::parse(initial_source_line).unwrap().0.unwrap();
10479        let mut frontend_line = FrontendState::new();
10480        frontend_line.hack_set_program(&ctx, program_line).await.unwrap();
10481        let sketch_object_line = find_first_sketch_object(&frontend_line.scene_graph).unwrap();
10482        let sketch_id_line = sketch_object_line.id;
10483        let sketch_line = expect_sketch(sketch_object_line);
10484        let line_id = *sketch_line.segments.first().unwrap();
10485
10486        let constraint_line = Constraint::Radius(Radius {
10487            arc: line_id,
10488            radius: Number {
10489                value: 5.0,
10490                units: NumericSuffix::Mm,
10491            },
10492            label_position: None,
10493            source: Default::default(),
10494        });
10495        let result_line = frontend_line
10496            .add_constraint(&mock_ctx, version, sketch_id_line, constraint_line)
10497            .await;
10498        assert!(result_line.is_err(), "Single line segment should error for radius");
10499
10500        ctx.close().await;
10501        mock_ctx.close().await;
10502    }
10503
10504    #[tokio::test(flavor = "multi_thread")]
10505    async fn test_diameter_single_arc_segment() {
10506        let initial_source = "\
10507sketch(on = XY) {
10508  arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
10509}
10510";
10511
10512        let program = Program::parse(initial_source).unwrap().0.unwrap();
10513
10514        let mut frontend = FrontendState::new();
10515
10516        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10517        let mock_ctx = ExecutorContext::new_mock(None).await;
10518        let version = Version(0);
10519
10520        frontend.hack_set_program(&ctx, program).await.unwrap();
10521        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10522        let sketch_id = sketch_object.id;
10523        let sketch = expect_sketch(sketch_object);
10524        // Find the arc segment (not the points)
10525        let arc_id = sketch
10526            .segments
10527            .iter()
10528            .find(|&seg_id| {
10529                let obj = frontend.scene_graph.objects.get(seg_id.0);
10530                matches!(
10531                    obj.map(|o| &o.kind),
10532                    Some(ObjectKind::Segment {
10533                        segment: Segment::Arc(_)
10534                    })
10535                )
10536            })
10537            .unwrap();
10538
10539        let constraint = Constraint::Diameter(Diameter {
10540            arc: *arc_id,
10541            diameter: Number {
10542                value: 10.0,
10543                units: NumericSuffix::Mm,
10544            },
10545            label_position: None,
10546            source: Default::default(),
10547        });
10548        let (src_delta, scene_delta) = frontend
10549            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10550            .await
10551            .unwrap();
10552        assert_eq!(
10553            src_delta.text.as_str(),
10554            // The lack indentation is a formatter bug.
10555            "\
10556sketch(on = XY) {
10557  arc1 = arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
10558  diameter(arc1) == 10mm
10559}
10560"
10561        );
10562        assert_eq!(
10563            scene_delta.new_graph.objects.len(),
10564            7, // Plane (0) + Sketch (1) + Start point (2) + End point (3) + Center point (4) + Arc (5) + Constraint (6)
10565            "{:#?}",
10566            scene_delta.new_graph.objects
10567        );
10568
10569        ctx.close().await;
10570        mock_ctx.close().await;
10571    }
10572
10573    #[tokio::test(flavor = "multi_thread")]
10574    async fn test_diameter_single_arc_segment_with_label_position() {
10575        let initial_source = "\
10576sketch(on = XY) {
10577  arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
10578}
10579";
10580
10581        let program = Program::parse(initial_source).unwrap().0.unwrap();
10582        let mut frontend = FrontendState::new();
10583        let mock_ctx = ExecutorContext::new_mock(None).await;
10584        let version = Version(0);
10585
10586        frontend.program = program.clone();
10587        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10588        frontend.update_state_after_exec(outcome, true);
10589        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10590        let sketch_id = sketch_object.id;
10591        let sketch = expect_sketch(sketch_object);
10592        let arc_id = sketch
10593            .segments
10594            .iter()
10595            .find(|&seg_id| {
10596                let obj = frontend.scene_graph.objects.get(seg_id.0);
10597                matches!(
10598                    obj.map(|o| &o.kind),
10599                    Some(ObjectKind::Segment {
10600                        segment: Segment::Arc(_)
10601                    })
10602                )
10603            })
10604            .unwrap();
10605
10606        let label_position = Point2d {
10607            x: Number {
10608                value: 10.0,
10609                units: NumericSuffix::Mm,
10610            },
10611            y: Number {
10612                value: 11.0,
10613                units: NumericSuffix::Mm,
10614            },
10615        };
10616        let constraint = Constraint::Diameter(Diameter {
10617            arc: *arc_id,
10618            diameter: Number {
10619                value: 10.0,
10620                units: NumericSuffix::Mm,
10621            },
10622            label_position: Some(label_position.clone()),
10623            source: Default::default(),
10624        });
10625        let (src_delta, scene_delta) = frontend
10626            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10627            .await
10628            .unwrap();
10629        assert_eq!(
10630            src_delta.text.as_str(),
10631            "\
10632sketch(on = XY) {
10633  arc1 = arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
10634  diameter(arc1, labelPosition = [10mm, 11mm]) == 10mm
10635}
10636"
10637        );
10638
10639        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10640        let sketch = expect_sketch(sketch_object);
10641        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10642        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10643            panic!("Expected constraint object");
10644        };
10645        let Constraint::Diameter(diameter) = constraint else {
10646            panic!("Expected diameter constraint");
10647        };
10648        assert_eq!(diameter.label_position, Some(label_position));
10649
10650        mock_ctx.close().await;
10651    }
10652
10653    #[tokio::test(flavor = "multi_thread")]
10654    async fn test_edit_diameter_constraint_label_position() {
10655        let initial_source = "\
10656sketch(on = XY) {
10657  arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
10658  diameter(arc1) == 10mm
10659}
10660";
10661
10662        let program = Program::parse(initial_source).unwrap().0.unwrap();
10663        let mut frontend = FrontendState::new();
10664        let mock_ctx = ExecutorContext::new_mock(None).await;
10665        let version = Version(0);
10666
10667        frontend.program = program.clone();
10668        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10669        frontend.update_state_after_exec(outcome, true);
10670        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10671        let sketch_id = sketch_object.id;
10672        let sketch = expect_sketch(sketch_object);
10673        let constraint_id = sketch.constraints[0];
10674        let label_position = Point2d {
10675            x: Number {
10676                value: 10.0,
10677                units: NumericSuffix::Mm,
10678            },
10679            y: Number {
10680                value: 11.0,
10681                units: NumericSuffix::Mm,
10682            },
10683        };
10684
10685        let (src_delta, scene_delta) = frontend
10686            .edit_distance_constraint_label_position(
10687                &mock_ctx,
10688                version,
10689                sketch_id,
10690                constraint_id,
10691                label_position.clone(),
10692                vec![],
10693            )
10694            .await
10695            .unwrap();
10696        assert_eq!(
10697            src_delta.text.as_str(),
10698            "\
10699sketch(on = XY) {
10700  arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
10701  diameter(arc1, labelPosition = [10mm, 11mm]) == 10mm
10702}
10703"
10704        );
10705
10706        let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
10707        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10708            panic!("Expected constraint object");
10709        };
10710        let Constraint::Diameter(diameter) = constraint else {
10711            panic!("Expected diameter constraint");
10712        };
10713        assert_eq!(diameter.label_position, Some(label_position));
10714
10715        mock_ctx.close().await;
10716    }
10717
10718    #[tokio::test(flavor = "multi_thread")]
10719    async fn test_diameter_error_cases() {
10720        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10721        let mock_ctx = ExecutorContext::new_mock(None).await;
10722        let version = Version(0);
10723
10724        // Test: Single point should error
10725        let initial_source_point = "\
10726sketch(on = XY) {
10727  point(at = [var 1, var 2])
10728}
10729";
10730        let program_point = Program::parse(initial_source_point).unwrap().0.unwrap();
10731        let mut frontend_point = FrontendState::new();
10732        frontend_point.hack_set_program(&ctx, program_point).await.unwrap();
10733        let sketch_object_point = find_first_sketch_object(&frontend_point.scene_graph).unwrap();
10734        let sketch_id_point = sketch_object_point.id;
10735        let sketch_point = expect_sketch(sketch_object_point);
10736        let point_id = *sketch_point.segments.first().unwrap();
10737
10738        let constraint_point = Constraint::Diameter(Diameter {
10739            arc: point_id,
10740            diameter: Number {
10741                value: 10.0,
10742                units: NumericSuffix::Mm,
10743            },
10744            label_position: None,
10745            source: Default::default(),
10746        });
10747        let result_point = frontend_point
10748            .add_constraint(&mock_ctx, version, sketch_id_point, constraint_point)
10749            .await;
10750        assert!(result_point.is_err(), "Single point should error for diameter");
10751
10752        // Test: Single line segment should error (only arc segments supported)
10753        let initial_source_line = "\
10754sketch(on = XY) {
10755  line(start = [var 1, var 2], end = [var 3, var 4])
10756}
10757";
10758        let program_line = Program::parse(initial_source_line).unwrap().0.unwrap();
10759        let mut frontend_line = FrontendState::new();
10760        frontend_line.hack_set_program(&ctx, program_line).await.unwrap();
10761        let sketch_object_line = find_first_sketch_object(&frontend_line.scene_graph).unwrap();
10762        let sketch_id_line = sketch_object_line.id;
10763        let sketch_line = expect_sketch(sketch_object_line);
10764        let line_id = *sketch_line.segments.first().unwrap();
10765
10766        let constraint_line = Constraint::Diameter(Diameter {
10767            arc: line_id,
10768            diameter: Number {
10769                value: 10.0,
10770                units: NumericSuffix::Mm,
10771            },
10772            label_position: None,
10773            source: Default::default(),
10774        });
10775        let result_line = frontend_line
10776            .add_constraint(&mock_ctx, version, sketch_id_line, constraint_line)
10777            .await;
10778        assert!(result_line.is_err(), "Single line segment should error for diameter");
10779
10780        ctx.close().await;
10781        mock_ctx.close().await;
10782    }
10783
10784    #[tokio::test(flavor = "multi_thread")]
10785    async fn test_line_horizontal() {
10786        let initial_source = "\
10787sketch(on = XY) {
10788  line(start = [var 1, var 2], end = [var 3, var 4])
10789}
10790";
10791
10792        let program = Program::parse(initial_source).unwrap().0.unwrap();
10793
10794        let mut frontend = FrontendState::new();
10795
10796        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10797        let mock_ctx = ExecutorContext::new_mock(None).await;
10798        let version = Version(0);
10799
10800        frontend.hack_set_program(&ctx, program).await.unwrap();
10801        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10802        let sketch_id = sketch_object.id;
10803        let sketch = expect_sketch(sketch_object);
10804        let line1_id = *sketch.segments.get(2).unwrap();
10805
10806        let constraint = Constraint::Horizontal(Horizontal::Line { line: line1_id });
10807        let (src_delta, scene_delta) = frontend
10808            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10809            .await
10810            .unwrap();
10811        assert_eq!(
10812            src_delta.text.as_str(),
10813            "\
10814sketch(on = XY) {
10815  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10816  horizontal(line1)
10817}
10818"
10819        );
10820        assert_eq!(
10821            scene_delta.new_graph.objects.len(),
10822            6,
10823            "{:#?}",
10824            scene_delta.new_graph.objects
10825        );
10826
10827        ctx.close().await;
10828        mock_ctx.close().await;
10829    }
10830
10831    #[tokio::test(flavor = "multi_thread")]
10832    async fn test_line_vertical() {
10833        let initial_source = "\
10834sketch(on = XY) {
10835  line(start = [var 1, var 2], end = [var 3, var 4])
10836}
10837";
10838
10839        let program = Program::parse(initial_source).unwrap().0.unwrap();
10840
10841        let mut frontend = FrontendState::new();
10842
10843        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10844        let mock_ctx = ExecutorContext::new_mock(None).await;
10845        let version = Version(0);
10846
10847        frontend.hack_set_program(&ctx, program).await.unwrap();
10848        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10849        let sketch_id = sketch_object.id;
10850        let sketch = expect_sketch(sketch_object);
10851        let line1_id = *sketch.segments.get(2).unwrap();
10852
10853        let constraint = Constraint::Vertical(Vertical::Line { line: line1_id });
10854        let (src_delta, scene_delta) = frontend
10855            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10856            .await
10857            .unwrap();
10858        assert_eq!(
10859            src_delta.text.as_str(),
10860            "\
10861sketch(on = XY) {
10862  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10863  vertical(line1)
10864}
10865"
10866        );
10867        assert_eq!(
10868            scene_delta.new_graph.objects.len(),
10869            6,
10870            "{:#?}",
10871            scene_delta.new_graph.objects
10872        );
10873
10874        ctx.close().await;
10875        mock_ctx.close().await;
10876    }
10877
10878    #[tokio::test(flavor = "multi_thread")]
10879    async fn test_points_vertical() {
10880        let initial_source = "\
10881sketch001 = sketch(on = XY) {
10882  p0 = point(at = [var -2.23mm, var 3.1mm])
10883  pf = point(at = [4, 4])
10884}
10885";
10886
10887        let program = Program::parse(initial_source).unwrap().0.unwrap();
10888
10889        let mut frontend = FrontendState::new();
10890
10891        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10892        let mock_ctx = ExecutorContext::new_mock(None).await;
10893        let version = Version(0);
10894
10895        frontend.hack_set_program(&ctx, program).await.unwrap();
10896        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10897        let sketch_id = sketch_object.id;
10898        let sketch = expect_sketch(sketch_object);
10899        let point_ids = vec![
10900            sketch.segments.first().unwrap().to_owned(),
10901            sketch.segments.get(1).unwrap().to_owned(),
10902        ];
10903
10904        let constraint = Constraint::Vertical(Vertical::Points {
10905            points: point_ids.into_iter().map(ConstraintSegment::from).collect(),
10906        });
10907        let (src_delta, scene_delta) = frontend
10908            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10909            .await
10910            .unwrap();
10911        assert_eq!(
10912            src_delta.text.as_str(),
10913            "\
10914sketch001 = sketch(on = XY) {
10915  p0 = point(at = [var -2.23mm, var 3.1mm])
10916  pf = point(at = [4, 4])
10917  vertical([p0, pf])
10918}
10919"
10920        );
10921        assert_eq!(
10922            scene_delta.new_graph.objects.len(),
10923            5,
10924            "{:#?}",
10925            scene_delta.new_graph.objects
10926        );
10927
10928        ctx.close().await;
10929        mock_ctx.close().await;
10930    }
10931
10932    #[tokio::test(flavor = "multi_thread")]
10933    async fn test_points_horizontal() {
10934        let initial_source = "\
10935sketch001 = sketch(on = XY) {
10936  p0 = point(at = [var -2.23mm, var 3.1mm])
10937  pf = point(at = [4, 4])
10938}
10939";
10940
10941        let program = Program::parse(initial_source).unwrap().0.unwrap();
10942
10943        let mut frontend = FrontendState::new();
10944
10945        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10946        let mock_ctx = ExecutorContext::new_mock(None).await;
10947        let version = Version(0);
10948
10949        frontend.hack_set_program(&ctx, program).await.unwrap();
10950        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10951        let sketch_id = sketch_object.id;
10952        let sketch = expect_sketch(sketch_object);
10953        let point_ids = vec![
10954            sketch.segments.first().unwrap().to_owned(),
10955            sketch.segments.get(1).unwrap().to_owned(),
10956        ];
10957
10958        let constraint = Constraint::Horizontal(Horizontal::Points {
10959            points: point_ids.into_iter().map(ConstraintSegment::from).collect(),
10960        });
10961        let (src_delta, scene_delta) = frontend
10962            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10963            .await
10964            .unwrap();
10965        assert_eq!(
10966            src_delta.text.as_str(),
10967            "\
10968sketch001 = sketch(on = XY) {
10969  p0 = point(at = [var -2.23mm, var 3.1mm])
10970  pf = point(at = [4, 4])
10971  horizontal([p0, pf])
10972}
10973"
10974        );
10975        assert_eq!(
10976            scene_delta.new_graph.objects.len(),
10977            5,
10978            "{:#?}",
10979            scene_delta.new_graph.objects
10980        );
10981
10982        ctx.close().await;
10983        mock_ctx.close().await;
10984    }
10985
10986    #[tokio::test(flavor = "multi_thread")]
10987    async fn test_point_horizontal_with_origin() {
10988        let initial_source = "\
10989sketch001 = sketch(on = XY) {
10990  p0 = point(at = [var -2.23mm, var 3.1mm])
10991}
10992";
10993
10994        let program = Program::parse(initial_source).unwrap().0.unwrap();
10995
10996        let mut frontend = FrontendState::new();
10997
10998        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10999        let mock_ctx = ExecutorContext::new_mock(None).await;
11000        let version = Version(0);
11001
11002        frontend.hack_set_program(&ctx, program).await.unwrap();
11003        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11004        let sketch_id = sketch_object.id;
11005        let sketch = expect_sketch(sketch_object);
11006        let point_id = *sketch.segments.first().unwrap();
11007
11008        let constraint = Constraint::Horizontal(Horizontal::Points {
11009            points: vec![ConstraintSegment::from(point_id), ConstraintSegment::ORIGIN],
11010        });
11011        let (src_delta, scene_delta) = frontend
11012            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11013            .await
11014            .unwrap();
11015        assert_eq!(
11016            src_delta.text.as_str(),
11017            "\
11018sketch001 = sketch(on = XY) {
11019  p0 = point(at = [var -2.23mm, var 3.1mm])
11020  horizontal([p0, ORIGIN])
11021}
11022"
11023        );
11024        assert_eq!(
11025            scene_delta.new_graph.objects.len(),
11026            4,
11027            "{:#?}",
11028            scene_delta.new_graph.objects
11029        );
11030
11031        ctx.close().await;
11032        mock_ctx.close().await;
11033    }
11034
11035    #[tokio::test(flavor = "multi_thread")]
11036    async fn test_lines_equal_length() {
11037        let initial_source = "\
11038sketch(on = XY) {
11039  line(start = [var 1, var 2], end = [var 3, var 4])
11040  line(start = [var 5, var 6], end = [var 7, var 8])
11041}
11042";
11043
11044        let program = Program::parse(initial_source).unwrap().0.unwrap();
11045
11046        let mut frontend = FrontendState::new();
11047
11048        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11049        let mock_ctx = ExecutorContext::new_mock(None).await;
11050        let version = Version(0);
11051
11052        frontend.hack_set_program(&ctx, program).await.unwrap();
11053        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11054        let sketch_id = sketch_object.id;
11055        let sketch = expect_sketch(sketch_object);
11056        let line1_id = *sketch.segments.get(2).unwrap();
11057        let line2_id = *sketch.segments.get(5).unwrap();
11058
11059        let constraint = Constraint::LinesEqualLength(LinesEqualLength {
11060            lines: vec![line1_id, line2_id],
11061        });
11062        let (src_delta, scene_delta) = frontend
11063            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11064            .await
11065            .unwrap();
11066        assert_eq!(
11067            src_delta.text.as_str(),
11068            "\
11069sketch(on = XY) {
11070  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
11071  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
11072  equalLength([line1, line2])
11073}
11074"
11075        );
11076        assert_eq!(
11077            scene_delta.new_graph.objects.len(),
11078            9,
11079            "{:#?}",
11080            scene_delta.new_graph.objects
11081        );
11082
11083        ctx.close().await;
11084        mock_ctx.close().await;
11085    }
11086
11087    #[tokio::test(flavor = "multi_thread")]
11088    async fn test_add_constraint_multi_line_equal_length() {
11089        let initial_source = "\
11090sketch(on = XY) {
11091  line(start = [var 1, var 2], end = [var 3, var 4])
11092  line(start = [var 5, var 6], end = [var 7, var 8])
11093  line(start = [var 9, var 10], end = [var 11, var 12])
11094}
11095";
11096
11097        let program = Program::parse(initial_source).unwrap().0.unwrap();
11098
11099        let mut frontend = FrontendState::new();
11100        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11101        let mock_ctx = ExecutorContext::new_mock(None).await;
11102        let version = Version(0);
11103
11104        frontend.hack_set_program(&ctx, program).await.unwrap();
11105        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11106        let sketch_id = sketch_object.id;
11107        let sketch = expect_sketch(sketch_object);
11108        let line1_id = *sketch.segments.get(2).unwrap();
11109        let line2_id = *sketch.segments.get(5).unwrap();
11110        let line3_id = *sketch.segments.get(8).unwrap();
11111
11112        let constraint = Constraint::LinesEqualLength(LinesEqualLength {
11113            lines: vec![line1_id, line2_id, line3_id],
11114        });
11115        let (src_delta, scene_delta) = frontend
11116            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11117            .await
11118            .unwrap();
11119        assert_eq!(
11120            src_delta.text.as_str(),
11121            "\
11122sketch(on = XY) {
11123  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
11124  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
11125  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
11126  equalLength([line1, line2, line3])
11127}
11128"
11129        );
11130        let constraints = scene_delta
11131            .new_graph
11132            .objects
11133            .iter()
11134            .filter_map(|obj| {
11135                let ObjectKind::Constraint { constraint } = &obj.kind else {
11136                    return None;
11137                };
11138                Some(constraint)
11139            })
11140            .collect::<Vec<_>>();
11141
11142        assert_eq!(constraints.len(), 1, "{:#?}", frontend.scene_graph.objects);
11143        let Constraint::LinesEqualLength(lines_equal_length) = constraints[0] else {
11144            panic!("expected equal length constraint, got {:?}", constraints[0]);
11145        };
11146        assert_eq!(lines_equal_length.lines.len(), 3);
11147
11148        ctx.close().await;
11149        mock_ctx.close().await;
11150    }
11151
11152    #[tokio::test(flavor = "multi_thread")]
11153    async fn test_lines_parallel() {
11154        let initial_source = "\
11155sketch(on = XY) {
11156  line(start = [var 1, var 2], end = [var 3, var 4])
11157  line(start = [var 5, var 6], end = [var 7, var 8])
11158}
11159";
11160
11161        let program = Program::parse(initial_source).unwrap().0.unwrap();
11162
11163        let mut frontend = FrontendState::new();
11164
11165        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11166        let mock_ctx = ExecutorContext::new_mock(None).await;
11167        let version = Version(0);
11168
11169        frontend.hack_set_program(&ctx, program).await.unwrap();
11170        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11171        let sketch_id = sketch_object.id;
11172        let sketch = expect_sketch(sketch_object);
11173        let line1_id = *sketch.segments.get(2).unwrap();
11174        let line2_id = *sketch.segments.get(5).unwrap();
11175
11176        let constraint = Constraint::Parallel(Parallel {
11177            lines: vec![line1_id, line2_id],
11178        });
11179        let (src_delta, scene_delta) = frontend
11180            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11181            .await
11182            .unwrap();
11183        assert_eq!(
11184            src_delta.text.as_str(),
11185            "\
11186sketch(on = XY) {
11187  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
11188  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
11189  parallel([line1, line2])
11190}
11191"
11192        );
11193        assert_eq!(
11194            scene_delta.new_graph.objects.len(),
11195            9,
11196            "{:#?}",
11197            scene_delta.new_graph.objects
11198        );
11199
11200        ctx.close().await;
11201        mock_ctx.close().await;
11202    }
11203
11204    #[tokio::test(flavor = "multi_thread")]
11205    async fn test_lines_parallel_multiline() {
11206        let initial_source = "\
11207sketch(on = XY) {
11208  line(start = [var 1, var 2], end = [var 3, var 4])
11209  line(start = [var 5, var 6], end = [var 7, var 8])
11210  line(start = [var 9, var 10], end = [var 11, var 12])
11211}
11212";
11213
11214        let program = Program::parse(initial_source).unwrap().0.unwrap();
11215
11216        let mut frontend = FrontendState::new();
11217
11218        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11219        let mock_ctx = ExecutorContext::new_mock(None).await;
11220        let version = Version(0);
11221
11222        frontend.hack_set_program(&ctx, program).await.unwrap();
11223        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11224        let sketch_id = sketch_object.id;
11225        let sketch = expect_sketch(sketch_object);
11226        let line1_id = *sketch.segments.get(2).unwrap();
11227        let line2_id = *sketch.segments.get(5).unwrap();
11228        let line3_id = *sketch.segments.get(8).unwrap();
11229
11230        let constraint = Constraint::Parallel(Parallel {
11231            lines: vec![line1_id, line2_id, line3_id],
11232        });
11233        let (src_delta, scene_delta) = frontend
11234            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11235            .await
11236            .unwrap();
11237        assert_eq!(
11238            src_delta.text.as_str(),
11239            "\
11240sketch(on = XY) {
11241  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
11242  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
11243  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
11244  parallel([line1, line2, line3])
11245}
11246"
11247        );
11248
11249        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
11250        let sketch = expect_sketch(sketch_object);
11251        assert_eq!(sketch.constraints.len(), 1);
11252
11253        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
11254        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11255            panic!("Expected constraint object");
11256        };
11257        let Constraint::Parallel(parallel) = constraint else {
11258            panic!("Expected parallel constraint");
11259        };
11260        assert_eq!(parallel.lines.len(), 3);
11261
11262        ctx.close().await;
11263        mock_ctx.close().await;
11264    }
11265
11266    #[tokio::test(flavor = "multi_thread")]
11267    async fn test_lines_perpendicular() {
11268        let initial_source = "\
11269sketch(on = XY) {
11270  line(start = [var 1, var 2], end = [var 3, var 4])
11271  line(start = [var 5, var 6], end = [var 7, var 8])
11272}
11273";
11274
11275        let program = Program::parse(initial_source).unwrap().0.unwrap();
11276
11277        let mut frontend = FrontendState::new();
11278
11279        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11280        let mock_ctx = ExecutorContext::new_mock(None).await;
11281        let version = Version(0);
11282
11283        frontend.hack_set_program(&ctx, program).await.unwrap();
11284        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11285        let sketch_id = sketch_object.id;
11286        let sketch = expect_sketch(sketch_object);
11287        let line1_id = *sketch.segments.get(2).unwrap();
11288        let line2_id = *sketch.segments.get(5).unwrap();
11289
11290        let constraint = Constraint::Perpendicular(Perpendicular {
11291            lines: vec![line1_id, line2_id],
11292        });
11293        let (src_delta, scene_delta) = frontend
11294            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11295            .await
11296            .unwrap();
11297        assert_eq!(
11298            src_delta.text.as_str(),
11299            "\
11300sketch(on = XY) {
11301  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
11302  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
11303  perpendicular([line1, line2])
11304}
11305"
11306        );
11307        assert_eq!(
11308            scene_delta.new_graph.objects.len(),
11309            9,
11310            "{:#?}",
11311            scene_delta.new_graph.objects
11312        );
11313
11314        ctx.close().await;
11315        mock_ctx.close().await;
11316    }
11317
11318    #[tokio::test(flavor = "multi_thread")]
11319    async fn test_lines_angle() {
11320        let initial_source = "\
11321sketch(on = XY) {
11322  line(start = [var 1, var 2], end = [var 3, var 4])
11323  line(start = [var 5, var 6], end = [var 7, var 8])
11324}
11325";
11326
11327        let program = Program::parse(initial_source).unwrap().0.unwrap();
11328
11329        let mut frontend = FrontendState::new();
11330
11331        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11332        let mock_ctx = ExecutorContext::new_mock(None).await;
11333        let version = Version(0);
11334
11335        frontend.hack_set_program(&ctx, program).await.unwrap();
11336        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11337        let sketch_id = sketch_object.id;
11338        let sketch = expect_sketch(sketch_object);
11339        let line1_id = *sketch.segments.get(2).unwrap();
11340        let line2_id = *sketch.segments.get(5).unwrap();
11341
11342        let constraint = Constraint::Angle(Angle {
11343            lines: vec![line1_id, line2_id],
11344            angle: Number {
11345                value: 30.0,
11346                units: NumericSuffix::Deg,
11347            },
11348            source: Default::default(),
11349        });
11350        let (src_delta, scene_delta) = frontend
11351            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11352            .await
11353            .unwrap();
11354        assert_eq!(
11355            src_delta.text.as_str(),
11356            // The lack indentation is a formatter bug.
11357            "\
11358sketch(on = XY) {
11359  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
11360  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
11361  angle([line1, line2]) == 30deg
11362}
11363"
11364        );
11365        assert_eq!(
11366            scene_delta.new_graph.objects.len(),
11367            9,
11368            "{:#?}",
11369            scene_delta.new_graph.objects
11370        );
11371
11372        ctx.close().await;
11373        mock_ctx.close().await;
11374    }
11375
11376    #[tokio::test(flavor = "multi_thread")]
11377    async fn test_segments_tangent() {
11378        let initial_source = "\
11379sketch(on = XY) {
11380  line(start = [var 1, var 2], end = [var 3, var 4])
11381  arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
11382}
11383";
11384
11385        let program = Program::parse(initial_source).unwrap().0.unwrap();
11386
11387        let mut frontend = FrontendState::new();
11388
11389        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11390        let mock_ctx = ExecutorContext::new_mock(None).await;
11391        let version = Version(0);
11392
11393        frontend.hack_set_program(&ctx, program).await.unwrap();
11394        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11395        let sketch_id = sketch_object.id;
11396        let sketch = expect_sketch(sketch_object);
11397        let line1_id = *sketch.segments.get(2).unwrap();
11398        let arc1_id = *sketch.segments.get(6).unwrap();
11399
11400        let constraint = Constraint::Tangent(Tangent {
11401            input: vec![line1_id, arc1_id],
11402        });
11403        let (src_delta, scene_delta) = frontend
11404            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11405            .await
11406            .unwrap();
11407        assert_eq!(
11408            src_delta.text.as_str(),
11409            "\
11410sketch(on = XY) {
11411  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
11412  arc1 = arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
11413  tangent([line1, arc1])
11414}
11415"
11416        );
11417        assert_eq!(
11418            scene_delta.new_graph.objects.len(),
11419            10,
11420            "{:#?}",
11421            scene_delta.new_graph.objects
11422        );
11423
11424        ctx.close().await;
11425        mock_ctx.close().await;
11426    }
11427
11428    #[tokio::test(flavor = "multi_thread")]
11429    async fn test_point_midpoint() {
11430        let initial_source = "\
11431sketch(on = XY) {
11432  point(at = [var 1, var 1])
11433  line(start = [var 0, var 0], end = [var 6, var 4])
11434}
11435";
11436
11437        let program = Program::parse(initial_source).unwrap().0.unwrap();
11438
11439        let mut frontend = FrontendState::new();
11440
11441        let ctx = ExecutorContext::new_mock(None).await;
11442        let version = Version(0);
11443
11444        frontend.program = program.clone();
11445        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11446        frontend.update_state_after_exec(outcome, true);
11447        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11448        let sketch_id = sketch_object.id;
11449        let sketch = expect_sketch(sketch_object);
11450        let point_id = *sketch.segments.first().unwrap();
11451        let line_id = *sketch.segments.get(3).unwrap();
11452
11453        let constraint = Constraint::Midpoint(Midpoint {
11454            point: point_id,
11455            segment: line_id,
11456        });
11457        let (src_delta, scene_delta) = frontend
11458            .add_constraint(&ctx, version, sketch_id, constraint)
11459            .await
11460            .unwrap();
11461        assert_eq!(
11462            src_delta.text.as_str(),
11463            "\
11464sketch(on = XY) {
11465  point1 = point(at = [var 1, var 1])
11466  line1 = line(start = [var 0, var 0], end = [var 6, var 4])
11467  midpoint(line1, point = point1)
11468}
11469"
11470        );
11471        assert_eq!(
11472            scene_delta.new_graph.objects.len(),
11473            7,
11474            "{:#?}",
11475            scene_delta.new_graph.objects
11476        );
11477
11478        ctx.close().await;
11479    }
11480
11481    #[tokio::test(flavor = "multi_thread")]
11482    async fn test_segments_symmetric() {
11483        let initial_source = "\
11484sketch(on = XY) {
11485  line(start = [var 0, var 0], end = [var 0, var 4])
11486  line(start = [var 4, var 0], end = [var 4, var 4])
11487  line(start = [var 2, var -1], end = [var 2, var 5])
11488}
11489";
11490
11491        let program = Program::parse(initial_source).unwrap().0.unwrap();
11492
11493        let mut frontend = FrontendState::new();
11494
11495        let ctx = ExecutorContext::new_mock(None).await;
11496        let version = Version(0);
11497
11498        frontend.program = program.clone();
11499        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11500        frontend.update_state_after_exec(outcome, true);
11501        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11502        let sketch_id = sketch_object.id;
11503        let sketch = expect_sketch(sketch_object);
11504        let line1_id = *sketch.segments.get(2).unwrap();
11505        let line2_id = *sketch.segments.get(5).unwrap();
11506        let axis_id = *sketch.segments.get(8).unwrap();
11507
11508        let constraint = Constraint::Symmetric(Symmetric {
11509            input: vec![line1_id, line2_id],
11510            axis: axis_id,
11511        });
11512        let (src_delta, scene_delta) = frontend
11513            .add_constraint(&ctx, version, sketch_id, constraint)
11514            .await
11515            .unwrap();
11516        assert_eq!(
11517            src_delta.text.as_str(),
11518            "\
11519sketch(on = XY) {
11520  line1 = line(start = [var 0, var 0], end = [var 0, var 4])
11521  line2 = line(start = [var 4, var 0], end = [var 4, var 4])
11522  line3 = line(start = [var 2, var -1], end = [var 2, var 5])
11523  symmetric([line1, line2], axis = line3)
11524}
11525"
11526        );
11527        assert_eq!(
11528            scene_delta.new_graph.objects.len(),
11529            12,
11530            "{:#?}",
11531            scene_delta.new_graph.objects
11532        );
11533
11534        ctx.close().await;
11535    }
11536
11537    #[tokio::test(flavor = "multi_thread")]
11538    async fn test_point_arc_midpoint() {
11539        let initial_source = "\
11540sketch(on = XY) {
11541  point(at = [var 6, var 3])
11542  arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
11543}
11544";
11545
11546        let program = Program::parse(initial_source).unwrap().0.unwrap();
11547
11548        let mut frontend = FrontendState::new();
11549
11550        let ctx = ExecutorContext::new_mock(None).await;
11551        let version = Version(0);
11552
11553        frontend.program = program.clone();
11554        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11555        frontend.update_state_after_exec(outcome, true);
11556        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11557        let sketch_id = sketch_object.id;
11558        let sketch = expect_sketch(sketch_object);
11559        let point_id = *sketch.segments.first().unwrap();
11560        let arc_id = *sketch.segments.get(4).unwrap();
11561
11562        let constraint = Constraint::Midpoint(Midpoint {
11563            point: point_id,
11564            segment: arc_id,
11565        });
11566        let (src_delta, scene_delta) = frontend
11567            .add_constraint(&ctx, version, sketch_id, constraint)
11568            .await
11569            .unwrap();
11570        assert_eq!(
11571            src_delta.text.as_str(),
11572            "\
11573sketch(on = XY) {
11574  point1 = point(at = [var 6, var 3])
11575  arc1 = arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
11576  midpoint(arc1, point = point1)
11577}
11578"
11579        );
11580        assert_eq!(
11581            scene_delta.new_graph.objects.len(),
11582            8,
11583            "{:#?}",
11584            scene_delta.new_graph.objects
11585        );
11586
11587        ctx.close().await;
11588    }
11589
11590    #[tokio::test(flavor = "multi_thread")]
11591    async fn test_segments_symmetric_arcs() {
11592        let initial_source = "\
11593sketch(on = XY) {
11594  arc(start = [var -15, var 0], end = [var -10, var 5], center = [var -10, var 0])
11595  arc(start = [var 6, var 2], end = [var 12, var -4], center = [var 8, var 1])
11596  line(start = [var 0, var -10], end = [var 0, var 10])
11597}
11598";
11599
11600        let program = Program::parse(initial_source).unwrap().0.unwrap();
11601
11602        let mut frontend = FrontendState::new();
11603
11604        let ctx = ExecutorContext::new_mock(None).await;
11605        let version = Version(0);
11606
11607        frontend.program = program.clone();
11608        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11609        frontend.update_state_after_exec(outcome, true);
11610        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11611        let sketch_id = sketch_object.id;
11612        let sketch = expect_sketch(sketch_object);
11613        let arc1_id = *sketch.segments.get(3).unwrap();
11614        let arc2_id = *sketch.segments.get(7).unwrap();
11615        let axis_id = *sketch.segments.get(10).unwrap();
11616
11617        let constraint = Constraint::Symmetric(Symmetric {
11618            input: vec![arc1_id, arc2_id],
11619            axis: axis_id,
11620        });
11621        let (src_delta, scene_delta) = frontend
11622            .add_constraint(&ctx, version, sketch_id, constraint)
11623            .await
11624            .unwrap();
11625        assert_eq!(
11626            src_delta.text.as_str(),
11627            "\
11628sketch(on = XY) {
11629  arc1 = arc(start = [var -15, var 0], end = [var -10, var 5], center = [var -10, var 0])
11630  arc2 = arc(start = [var 6, var 2], end = [var 12, var -4], center = [var 8, var 1])
11631  line1 = line(start = [var 0, var -10], end = [var 0, var 10])
11632  symmetric([arc1, arc2], axis = line1)
11633}
11634"
11635        );
11636        assert_eq!(
11637            scene_delta.new_graph.objects.len(),
11638            14,
11639            "{:#?}",
11640            scene_delta.new_graph.objects
11641        );
11642
11643        ctx.close().await;
11644    }
11645
11646    #[tokio::test(flavor = "multi_thread")]
11647    async fn test_sketch_on_face_simple() {
11648        let initial_source = "\
11649len = 2mm
11650cube = startSketchOn(XY)
11651  |> startProfile(at = [0, 0])
11652  |> line(end = [len, 0], tag = $side)
11653  |> line(end = [0, len])
11654  |> line(end = [-len, 0])
11655  |> line(end = [0, -len])
11656  |> close()
11657  |> extrude(length = len)
11658
11659face = faceOf(cube, face = side)
11660";
11661
11662        let program = Program::parse(initial_source).unwrap().0.unwrap();
11663
11664        let mut frontend = FrontendState::new();
11665
11666        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11667        let mock_ctx = ExecutorContext::new_mock(None).await;
11668        let version = Version(0);
11669
11670        frontend.hack_set_program(&ctx, program).await.unwrap();
11671        let face_object = find_first_face_object(&frontend.scene_graph).unwrap();
11672        let face_id = face_object.id;
11673
11674        let sketch_args = SketchCtor {
11675            on: Plane::Object(face_id),
11676        };
11677        let (_src_delta, scene_delta, sketch_id) = frontend
11678            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
11679            .await
11680            .unwrap();
11681        assert_eq!(sketch_id, ObjectId(2));
11682        assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
11683        let sketch_object = &scene_delta.new_graph.objects[2];
11684        assert_eq!(sketch_object.id, ObjectId(2));
11685        assert_eq!(
11686            sketch_object.kind,
11687            ObjectKind::Sketch(Sketch {
11688                args: SketchCtor {
11689                    on: Plane::Object(face_id),
11690                },
11691                plane: face_id,
11692                segments: vec![],
11693                constraints: vec![],
11694            })
11695        );
11696        assert_eq!(scene_delta.new_graph.objects.len(), 8);
11697
11698        ctx.close().await;
11699        mock_ctx.close().await;
11700    }
11701
11702    #[tokio::test(flavor = "multi_thread")]
11703    async fn test_sketch_on_wall_artifact_from_region_extrude() {
11704        let initial_source = "\
11705s = sketch(on = YZ) {
11706  line1 = line(start = [0, 0], end = [0, 1])
11707  line2 = line(start = [0, 1], end = [1, 1])
11708  line3 = line(start = [1, 1], end = [0, 0])
11709}
11710region001 = region(point = [0.1, 0.1], sketch = s)
11711extrude001 = extrude(region001, length = 5)
11712";
11713
11714        let program = Program::parse(initial_source).unwrap().0.unwrap();
11715
11716        let mut frontend = FrontendState::new();
11717        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11718        let version = Version(0);
11719
11720        frontend.hack_set_program(&ctx, program).await.unwrap();
11721        let wall_object_id = find_first_wall_object_id(&frontend.scene_graph).expect("expected a wall object");
11722
11723        let sketch_args = SketchCtor {
11724            on: Plane::Object(wall_object_id),
11725        };
11726        let (src_delta, _scene_delta, _sketch_id) = frontend
11727            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
11728            .await
11729            .unwrap();
11730        assert!(src_delta.text.contains("faceOf(extrude001, face = region001.tags."));
11731
11732        ctx.close().await;
11733    }
11734
11735    #[tokio::test(flavor = "multi_thread")]
11736    async fn test_sketch_on_wall_artifact_from_split_region_extrude() {
11737        let initial_source = "\
11738sketch001 = sketch(on = YZ) {
11739  line1 = line(start = [var 0.49, var -0.39], end = [var 6.52, var -0.39])
11740  line2 = line(start = [var 6.52, var -0.39], end = [var 6.52, var 4.9])
11741  line3 = line(start = [var 6.52, var 4.9], end = [var 0.49, var 4.9])
11742  line4 = line(start = [var 0.49, var 4.9], end = [var 0.49, var -0.39])
11743  coincident([line1.end, line2.start])
11744  coincident([line2.end, line3.start])
11745  coincident([line3.end, line4.start])
11746  coincident([line4.end, line1.start])
11747  parallel([line2, line4])
11748  parallel([line3, line1])
11749  perpendicular([line1, line2])
11750  horizontal(line3)
11751  line5 = line(start = [2.35, 6.65], end = [5.89, -2.7])
11752}
11753region001 = region(point = [3.1, 3.74], sketch = sketch001)
11754extrude001 = extrude(region001, length = 5)
11755";
11756
11757        let program = Program::parse(initial_source).unwrap().0.unwrap();
11758
11759        let mut frontend = FrontendState::new();
11760        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11761        let version = Version(0);
11762
11763        frontend.hack_set_program(&ctx, program).await.unwrap();
11764        let wall_object_id = find_first_wall_object_id(&frontend.scene_graph).expect("expected a wall object");
11765
11766        let sketch_args = SketchCtor {
11767            on: Plane::Object(wall_object_id),
11768        };
11769        let (src_delta, _scene_delta, _sketch_id) = frontend
11770            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
11771            .await
11772            .unwrap();
11773        assert!(src_delta.text.contains("faceOf(extrude001, face = region001.tags."));
11774
11775        ctx.close().await;
11776    }
11777
11778    #[tokio::test(flavor = "multi_thread")]
11779    async fn test_sketch_on_plane_incremental() {
11780        let initial_source = "\
11781len = 2mm
11782cube = startSketchOn(XY)
11783  |> startProfile(at = [0, 0])
11784  |> line(end = [len, 0], tag = $side)
11785  |> line(end = [0, len])
11786  |> line(end = [-len, 0])
11787  |> line(end = [0, -len])
11788  |> close()
11789  |> extrude(length = len)
11790
11791plane = planeOf(cube, face = side)
11792";
11793
11794        let program = Program::parse(initial_source).unwrap().0.unwrap();
11795
11796        let mut frontend = FrontendState::new();
11797
11798        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11799        let mock_ctx = ExecutorContext::new_mock(None).await;
11800        let version = Version(0);
11801
11802        frontend.hack_set_program(&ctx, program).await.unwrap();
11803        // Find the last plane since the first plane is the XY plane.
11804        let plane_object = frontend
11805            .scene_graph
11806            .objects
11807            .iter()
11808            .rev()
11809            .find(|object| matches!(&object.kind, ObjectKind::Plane(_)))
11810            .unwrap();
11811        let plane_id = plane_object.id;
11812
11813        let sketch_args = SketchCtor {
11814            on: Plane::Object(plane_id),
11815        };
11816        let (src_delta, scene_delta, sketch_id) = frontend
11817            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
11818            .await
11819            .unwrap();
11820        assert_eq!(
11821            src_delta.text.as_str(),
11822            "\
11823len = 2mm
11824cube = startSketchOn(XY)
11825  |> startProfile(at = [0, 0])
11826  |> line(end = [len, 0], tag = $side)
11827  |> line(end = [0, len])
11828  |> line(end = [-len, 0])
11829  |> line(end = [0, -len])
11830  |> close()
11831  |> extrude(length = len)
11832
11833plane = planeOf(cube, face = side)
11834sketch001 = sketch(on = plane) {
11835}
11836"
11837        );
11838        assert_eq!(sketch_id, ObjectId(2));
11839        assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
11840        let sketch_object = &scene_delta.new_graph.objects[2];
11841        assert_eq!(sketch_object.id, ObjectId(2));
11842        assert_eq!(
11843            sketch_object.kind,
11844            ObjectKind::Sketch(Sketch {
11845                args: SketchCtor {
11846                    on: Plane::Object(plane_id),
11847                },
11848                plane: plane_id,
11849                segments: vec![],
11850                constraints: vec![],
11851            })
11852        );
11853        assert_eq!(scene_delta.new_graph.objects.len(), 9);
11854
11855        let plane_object = scene_delta.new_graph.objects.get(plane_id.0).unwrap();
11856        assert_eq!(plane_object.id, plane_id);
11857        assert_eq!(plane_object.kind, ObjectKind::Plane(Plane::Object(plane_id)));
11858
11859        ctx.close().await;
11860        mock_ctx.close().await;
11861    }
11862
11863    #[tokio::test(flavor = "multi_thread")]
11864    async fn test_new_sketch_uses_unique_variable_name() {
11865        let initial_source = "\
11866sketch1 = sketch(on = XY) {
11867}
11868";
11869
11870        let program = Program::parse(initial_source).unwrap().0.unwrap();
11871
11872        let mut frontend = FrontendState::new();
11873        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11874        let version = Version(0);
11875
11876        frontend.hack_set_program(&ctx, program).await.unwrap();
11877
11878        let sketch_args = SketchCtor {
11879            on: Plane::Default(PlaneName::Yz),
11880        };
11881        let (src_delta, _, _) = frontend
11882            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
11883            .await
11884            .unwrap();
11885
11886        assert_eq!(
11887            src_delta.text.as_str(),
11888            "\
11889sketch1 = sketch(on = XY) {
11890}
11891sketch001 = sketch(on = YZ) {
11892}
11893"
11894        );
11895
11896        ctx.close().await;
11897    }
11898
11899    #[tokio::test(flavor = "multi_thread")]
11900    async fn test_new_sketch_twice_using_same_plane() {
11901        let initial_source = "\
11902sketch1 = sketch(on = XY) {
11903}
11904";
11905
11906        let program = Program::parse(initial_source).unwrap().0.unwrap();
11907
11908        let mut frontend = FrontendState::new();
11909        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11910        let version = Version(0);
11911
11912        frontend.hack_set_program(&ctx, program).await.unwrap();
11913
11914        let sketch_args = SketchCtor {
11915            on: Plane::Default(PlaneName::Xy),
11916        };
11917        let (src_delta, _, _) = frontend
11918            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
11919            .await
11920            .unwrap();
11921
11922        assert_eq!(
11923            src_delta.text.as_str(),
11924            "\
11925sketch1 = sketch(on = XY) {
11926}
11927sketch001 = sketch(on = XY) {
11928}
11929"
11930        );
11931
11932        ctx.close().await;
11933    }
11934
11935    #[tokio::test(flavor = "multi_thread")]
11936    async fn test_sketch_mode_reuses_cached_on_expression() {
11937        let initial_source = "\
11938width = 2mm
11939sketch(on = offsetPlane(XY, offset = width)) {
11940  line1 = line(start = [var 0, var 0], end = [var 1mm, var 0])
11941  distance([line1.start, line1.end]) == width
11942}
11943";
11944        let program = Program::parse(initial_source).unwrap().0.unwrap();
11945
11946        let mut frontend = FrontendState::new();
11947        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11948        let mock_ctx = ExecutorContext::new_mock(None).await;
11949        let version = Version(0);
11950        let project_id = ProjectId(0);
11951        let file_id = FileId(0);
11952
11953        frontend.hack_set_program(&ctx, program).await.unwrap();
11954        let initial_object_count = frontend.scene_graph.objects.len();
11955        let sketch_id = find_first_sketch_object(&frontend.scene_graph)
11956            .expect("Expected sketch object to exist")
11957            .id;
11958
11959        // Entering sketch mode should reuse cached `on` expression state
11960        // (offsetPlane result), not fail or create extra on-surface objects.
11961        let scene_delta = frontend
11962            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
11963            .await
11964            .unwrap();
11965        assert_eq!(scene_delta.new_graph.objects.len(), initial_object_count);
11966
11967        // A follow-up sketch-mode execution should keep the same stable object
11968        // graph shape as well.
11969        let (_src_delta, scene_delta) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
11970        assert_eq!(scene_delta.new_graph.objects.len(), initial_object_count);
11971
11972        ctx.close().await;
11973        mock_ctx.close().await;
11974    }
11975
11976    #[tokio::test(flavor = "multi_thread")]
11977    async fn test_multiple_sketch_blocks() {
11978        let initial_source = "\
11979// Cube that requires the engine.
11980width = 2
11981sketch001 = startSketchOn(XY)
11982profile001 = startProfile(sketch001, at = [0, 0])
11983  |> yLine(length = width, tag = $seg1)
11984  |> xLine(length = width)
11985  |> yLine(length = -width)
11986  |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
11987  |> close()
11988extrude001 = extrude(profile001, length = width)
11989
11990// Get a value that requires the engine.
11991x = segLen(seg1)
11992
11993// Triangle with side length 2*x.
11994sketch(on = XY) {
11995  line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
11996  line2 = line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
11997  coincident([line1.end, line2.start])
11998  line3 = line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
11999  coincident([line2.end, line3.start])
12000  coincident([line3.end, line1.start])
12001  equalLength([line3, line1])
12002  equalLength([line1, line2])
12003  distance([line1.start, line1.end]) == 2*x
12004}
12005
12006// Line segment with length x.
12007sketch2 = sketch(on = XY) {
12008  line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
12009  distance([line1.start, line1.end]) == x
12010}
12011";
12012
12013        let program = Program::parse(initial_source).unwrap().0.unwrap();
12014
12015        let mut frontend = FrontendState::new();
12016
12017        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12018        let mock_ctx = ExecutorContext::new_mock(None).await;
12019        let version = Version(0);
12020        let project_id = ProjectId(0);
12021        let file_id = FileId(0);
12022
12023        frontend.hack_set_program(&ctx, program).await.unwrap();
12024        let sketch_objects = frontend
12025            .scene_graph
12026            .objects
12027            .iter()
12028            .filter(|obj| matches!(obj.kind, ObjectKind::Sketch(_)))
12029            .collect::<Vec<_>>();
12030        let sketch1_id = sketch_objects.first().unwrap().id;
12031        let sketch2_id = sketch_objects.get(1).unwrap().id;
12032        // First point in sketch1.
12033        let point1_id = ObjectId(sketch1_id.0 + 1);
12034        // First point in sketch2.
12035        let point2_id = ObjectId(sketch2_id.0 + 1);
12036
12037        // Edit the first sketch. Objects before the sketch block should be
12038        // present from execution cache so that we can sketch on prior planes,
12039        // for example. Objects after the first sketch block should not be
12040        // present since those statements are skipped in sketch mode.
12041        //
12042        // - startSketchOn(XY) Plane 1
12043        // - sketch on=XY Plane 1
12044        // - Sketch block 16
12045        let scene_delta = frontend
12046            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch1_id)
12047            .await
12048            .unwrap();
12049        assert_eq!(
12050            scene_delta.new_graph.objects.len(),
12051            18,
12052            "{:#?}",
12053            scene_delta.new_graph.objects
12054        );
12055
12056        // Edit a point in the first sketch.
12057        let point_ctor = PointCtor {
12058            position: Point2d {
12059                x: Expr::Var(Number {
12060                    value: 1.0,
12061                    units: NumericSuffix::Mm,
12062                }),
12063                y: Expr::Var(Number {
12064                    value: 2.0,
12065                    units: NumericSuffix::Mm,
12066                }),
12067            },
12068        };
12069        let segments = vec![ExistingSegmentCtor {
12070            id: point1_id,
12071            ctor: SegmentCtor::Point(point_ctor),
12072        }];
12073        let (src_delta, _) = frontend
12074            .edit_segments(&mock_ctx, version, sketch1_id, segments)
12075            .await
12076            .unwrap();
12077        // Only the first sketch block changes.
12078        assert_eq!(
12079            src_delta.text.as_str(),
12080            "\
12081// Cube that requires the engine.
12082width = 2
12083sketch001 = startSketchOn(XY)
12084profile001 = startProfile(sketch001, at = [0, 0])
12085  |> yLine(length = width, tag = $seg1)
12086  |> xLine(length = width)
12087  |> yLine(length = -width)
12088  |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
12089  |> close()
12090extrude001 = extrude(profile001, length = width)
12091
12092// Get a value that requires the engine.
12093x = segLen(seg1)
12094
12095// Triangle with side length 2*x.
12096sketch(on = XY) {
12097  line1 = line(start = [var 1mm, var 2mm], end = [var 2.32mm, var -1.78mm])
12098  line2 = line(start = [var 2.32mm, var -1.78mm], end = [var -1.61mm, var -1.03mm])
12099  coincident([line1.end, line2.start])
12100  line3 = line(start = [var -1.61mm, var -1.03mm], end = [var 1mm, var 2mm])
12101  coincident([line2.end, line3.start])
12102  coincident([line3.end, line1.start])
12103  equalLength([line3, line1])
12104  equalLength([line1, line2])
12105  distance([line1.start, line1.end]) == 2 * x
12106}
12107
12108// Line segment with length x.
12109sketch2 = sketch(on = XY) {
12110  line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
12111  distance([line1.start, line1.end]) == x
12112}
12113"
12114        );
12115
12116        // Execute mock to simulate drag end.
12117        let (src_delta, _) = frontend.execute_mock(&mock_ctx, version, sketch1_id).await.unwrap();
12118        // Only the first sketch block changes.
12119        assert_eq!(
12120            src_delta.text.as_str(),
12121            "\
12122// Cube that requires the engine.
12123width = 2
12124sketch001 = startSketchOn(XY)
12125profile001 = startProfile(sketch001, at = [0, 0])
12126  |> yLine(length = width, tag = $seg1)
12127  |> xLine(length = width)
12128  |> yLine(length = -width)
12129  |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
12130  |> close()
12131extrude001 = extrude(profile001, length = width)
12132
12133// Get a value that requires the engine.
12134x = segLen(seg1)
12135
12136// Triangle with side length 2*x.
12137sketch(on = XY) {
12138  line1 = line(start = [var 1mm, var 2mm], end = [var 1.28mm, var -0.78mm])
12139  line2 = line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
12140  coincident([line1.end, line2.start])
12141  line3 = line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
12142  coincident([line2.end, line3.start])
12143  coincident([line3.end, line1.start])
12144  equalLength([line3, line1])
12145  equalLength([line1, line2])
12146  distance([line1.start, line1.end]) == 2 * x
12147}
12148
12149// Line segment with length x.
12150sketch2 = sketch(on = XY) {
12151  line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
12152  distance([line1.start, line1.end]) == x
12153}
12154"
12155        );
12156        // Exit sketch. Objects from the entire program should be present.
12157        //
12158        // - startSketchOn(XY) Plane 1
12159        // - sketch on=XY Plane 1
12160        // - Sketch block 16
12161        // - sketch on=XY cached
12162        // - Sketch block 5
12163        let scene = frontend.exit_sketch(&ctx, version, sketch1_id).await.unwrap();
12164        assert_eq!(scene.objects.len(), 30, "{:#?}", scene.objects);
12165
12166        // Edit the second sketch.
12167        //
12168        // - startSketchOn(XY) Plane 1
12169        // - sketch on=XY Plane 1
12170        // - Sketch block 16
12171        // - sketch on=XY cached
12172        // - Sketch block 5
12173        let scene_delta = frontend
12174            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch2_id)
12175            .await
12176            .unwrap();
12177        assert_eq!(
12178            scene_delta.new_graph.objects.len(),
12179            24,
12180            "{:#?}",
12181            scene_delta.new_graph.objects
12182        );
12183
12184        // Edit a point in the second sketch.
12185        let point_ctor = PointCtor {
12186            position: Point2d {
12187                x: Expr::Var(Number {
12188                    value: 3.0,
12189                    units: NumericSuffix::Mm,
12190                }),
12191                y: Expr::Var(Number {
12192                    value: 4.0,
12193                    units: NumericSuffix::Mm,
12194                }),
12195            },
12196        };
12197        let segments = vec![ExistingSegmentCtor {
12198            id: point2_id,
12199            ctor: SegmentCtor::Point(point_ctor),
12200        }];
12201        let (src_delta, _) = frontend
12202            .edit_segments(&mock_ctx, version, sketch2_id, segments)
12203            .await
12204            .unwrap();
12205        // Only the second sketch block changes.
12206        assert_eq!(
12207            src_delta.text.as_str(),
12208            "\
12209// Cube that requires the engine.
12210width = 2
12211sketch001 = startSketchOn(XY)
12212profile001 = startProfile(sketch001, at = [0, 0])
12213  |> yLine(length = width, tag = $seg1)
12214  |> xLine(length = width)
12215  |> yLine(length = -width)
12216  |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
12217  |> close()
12218extrude001 = extrude(profile001, length = width)
12219
12220// Get a value that requires the engine.
12221x = segLen(seg1)
12222
12223// Triangle with side length 2*x.
12224sketch(on = XY) {
12225  line1 = line(start = [var 1mm, var 2mm], end = [var 1.28mm, var -0.78mm])
12226  line2 = line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
12227  coincident([line1.end, line2.start])
12228  line3 = line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
12229  coincident([line2.end, line3.start])
12230  coincident([line3.end, line1.start])
12231  equalLength([line3, line1])
12232  equalLength([line1, line2])
12233  distance([line1.start, line1.end]) == 2 * x
12234}
12235
12236// Line segment with length x.
12237sketch2 = sketch(on = XY) {
12238  line1 = line(start = [var 3mm, var 4mm], end = [var 2.32mm, var 2.12mm])
12239  distance([line1.start, line1.end]) == x
12240}
12241"
12242        );
12243
12244        // Execute mock to simulate drag end.
12245        let (src_delta, _) = frontend.execute_mock(&mock_ctx, version, sketch2_id).await.unwrap();
12246        // Only the second sketch block changes.
12247        assert_eq!(
12248            src_delta.text.as_str(),
12249            "\
12250// Cube that requires the engine.
12251width = 2
12252sketch001 = startSketchOn(XY)
12253profile001 = startProfile(sketch001, at = [0, 0])
12254  |> yLine(length = width, tag = $seg1)
12255  |> xLine(length = width)
12256  |> yLine(length = -width)
12257  |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
12258  |> close()
12259extrude001 = extrude(profile001, length = width)
12260
12261// Get a value that requires the engine.
12262x = segLen(seg1)
12263
12264// Triangle with side length 2*x.
12265sketch(on = XY) {
12266  line1 = line(start = [var 1mm, var 2mm], end = [var 1.28mm, var -0.78mm])
12267  line2 = line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
12268  coincident([line1.end, line2.start])
12269  line3 = line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
12270  coincident([line2.end, line3.start])
12271  coincident([line3.end, line1.start])
12272  equalLength([line3, line1])
12273  equalLength([line1, line2])
12274  distance([line1.start, line1.end]) == 2 * x
12275}
12276
12277// Line segment with length x.
12278sketch2 = sketch(on = XY) {
12279  line1 = line(start = [var 3mm, var 4mm], end = [var 1.28mm, var -0.78mm])
12280  distance([line1.start, line1.end]) == x
12281}
12282"
12283        );
12284
12285        ctx.close().await;
12286        mock_ctx.close().await;
12287    }
12288
12289    #[tokio::test(flavor = "multi_thread")]
12290    async fn test_exit_sketch_without_changes_allows_entering_next_sketch() {
12291        clear_mem_cache().await;
12292
12293        let source = r#"sketch001 = sketch(on = XZ) {
12294  circle1 = circle(start = [var -1.96mm, var 2.77mm], center = [var -2.69mm, var 3.44mm])
12295}
12296sketch002 = sketch(on = XY) {
12297  line1 = line(start = [var 0mm, var 0mm], end = [var 4.68mm, var 0mm])
12298  line2 = line(start = [var 4.68mm, var 0mm], end = [var 4.68mm, var 2.96mm])
12299  line3 = line(start = [var 4.68mm, var 2.96mm], end = [var 0mm, var 2.96mm])
12300  line4 = line(start = [var 0mm, var 2.96mm], end = [var 0mm, var 0mm])
12301  coincident([line1.end, line2.start])
12302  coincident([line2.end, line3.start])
12303  coincident([line3.end, line4.start])
12304  coincident([line4.end, line1.start])
12305  parallel([line2, line4])
12306  parallel([line3, line1])
12307  perpendicular([line1, line2])
12308  horizontal(line3)
12309  coincident([line1.start, ORIGIN])
12310}
12311"#;
12312
12313        let program = Program::parse(source).unwrap().0.unwrap();
12314        let mut frontend = FrontendState::new();
12315        let ctx = ExecutorContext::new_with_engine(
12316            std::sync::Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().unwrap())),
12317            Default::default(),
12318        );
12319        let mock_ctx = ExecutorContext::new_mock(None).await;
12320        let version = Version(0);
12321        let project_id = ProjectId(0);
12322        let file_id = FileId(0);
12323
12324        frontend.hack_set_program(&ctx, program).await.unwrap();
12325        let sketch_objects = frontend
12326            .scene_graph
12327            .objects
12328            .iter()
12329            .filter(|object| matches!(object.kind, ObjectKind::Sketch(_)))
12330            .collect::<Vec<_>>();
12331        assert_eq!(sketch_objects.len(), 2, "{:#?}", frontend.scene_graph.objects);
12332
12333        let sketch1_id = sketch_objects[0].id;
12334        let sketch2_id = sketch_objects[1].id;
12335
12336        frontend
12337            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch1_id)
12338            .await
12339            .unwrap();
12340        frontend.exit_sketch(&ctx, version, sketch1_id).await.unwrap();
12341
12342        let scene_delta = frontend
12343            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch2_id)
12344            .await
12345            .unwrap();
12346        assert_eq!(scene_delta.new_graph.sketch_mode, Some(sketch2_id));
12347
12348        clear_mem_cache().await;
12349        ctx.close().await;
12350        mock_ctx.close().await;
12351    }
12352
12353    // Regression tests: operations on source code with extra whitespace/newlines.
12354    // These test that NodePath-based lookups work correctly when source ranges
12355    // are shifted by extra whitespace that wouldn't be present after formatting.
12356
12357    #[tokio::test(flavor = "multi_thread")]
12358    async fn test_extra_newlines_after_settings_edit_sketch_add_point() {
12359        // Extra newlines after @settings line - this shifts all source ranges.
12360        let initial_source = "@settings(defaultLengthUnit = mm)
12361
12362
12363
12364sketch001 = sketch(on = XY) {
12365  point(at = [1in, 2in])
12366}
12367";
12368
12369        let program = Program::parse(initial_source).unwrap().0.unwrap();
12370        let mut frontend = FrontendState::new();
12371
12372        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12373        let mock_ctx = ExecutorContext::new_mock(None).await;
12374        let version = Version(0);
12375        let project_id = ProjectId(0);
12376        let file_id = FileId(0);
12377
12378        frontend.hack_set_program(&ctx, program).await.unwrap();
12379        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12380        let sketch_id = sketch_object.id;
12381
12382        // Edit sketch should succeed despite extra newlines.
12383        frontend
12384            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
12385            .await
12386            .unwrap();
12387
12388        // Add a new point to the sketch.
12389        let point_ctor = PointCtor {
12390            position: Point2d {
12391                x: Expr::Number(Number {
12392                    value: 5.0,
12393                    units: NumericSuffix::Mm,
12394                }),
12395                y: Expr::Number(Number {
12396                    value: 6.0,
12397                    units: NumericSuffix::Mm,
12398                }),
12399            },
12400        };
12401        let segment = SegmentCtor::Point(point_ctor);
12402        let (src_delta, scene_delta) = frontend
12403            .add_segment(&mock_ctx, version, sketch_id, segment, None)
12404            .await
12405            .unwrap();
12406        // After adding a point, the source should be reformatted with standard whitespace.
12407        assert!(
12408            src_delta.text.contains("point(at = [5mm, 6mm])"),
12409            "Expected new point in source, got: {}",
12410            src_delta.text
12411        );
12412        assert!(!scene_delta.new_objects.is_empty());
12413
12414        ctx.close().await;
12415        mock_ctx.close().await;
12416    }
12417
12418    #[tokio::test(flavor = "multi_thread")]
12419    async fn test_extra_newlines_after_settings_add_line_to_empty_sketch() {
12420        // Extra newlines after @settings, with an empty sketch block.
12421        let initial_source = "@settings(defaultLengthUnit = mm)
12422
12423
12424
12425s = sketch(on = XY) {}
12426";
12427
12428        let program = Program::parse(initial_source).unwrap().0.unwrap();
12429        let mut frontend = FrontendState::new();
12430
12431        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12432        let mock_ctx = ExecutorContext::new_mock(None).await;
12433        let version = Version(0);
12434
12435        frontend.hack_set_program(&ctx, program).await.unwrap();
12436        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12437        let sketch_id = sketch_object.id;
12438
12439        let line_ctor = LineCtor {
12440            start: Point2d {
12441                x: Expr::Number(Number {
12442                    value: 0.0,
12443                    units: NumericSuffix::Mm,
12444                }),
12445                y: Expr::Number(Number {
12446                    value: 0.0,
12447                    units: NumericSuffix::Mm,
12448                }),
12449            },
12450            end: Point2d {
12451                x: Expr::Number(Number {
12452                    value: 10.0,
12453                    units: NumericSuffix::Mm,
12454                }),
12455                y: Expr::Number(Number {
12456                    value: 10.0,
12457                    units: NumericSuffix::Mm,
12458                }),
12459            },
12460            construction: None,
12461        };
12462        let segment = SegmentCtor::Line(line_ctor);
12463        let (src_delta, scene_delta) = frontend
12464            .add_segment(&mock_ctx, version, sketch_id, segment, None)
12465            .await
12466            .unwrap();
12467        assert!(
12468            src_delta.text.contains("line(start = [0mm, 0mm], end = [10mm, 10mm])"),
12469            "Expected line in source, got: {}",
12470            src_delta.text
12471        );
12472        // Line creates start point, end point, and line segment.
12473        assert_eq!(scene_delta.new_objects.len(), 3);
12474
12475        ctx.close().await;
12476        mock_ctx.close().await;
12477    }
12478
12479    #[tokio::test(flavor = "multi_thread")]
12480    async fn test_extra_newlines_between_operations_edit_line() {
12481        // Extra newlines between @settings and sketch, and inside the sketch block.
12482        let initial_source = "@settings(defaultLengthUnit = mm)
12483
12484
12485sketch001 = sketch(on = XY) {
12486
12487  line1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm])
12488
12489}
12490";
12491
12492        let program = Program::parse(initial_source).unwrap().0.unwrap();
12493        let mut frontend = FrontendState::new();
12494
12495        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12496        let mock_ctx = ExecutorContext::new_mock(None).await;
12497        let version = Version(0);
12498        let project_id = ProjectId(0);
12499        let file_id = FileId(0);
12500
12501        frontend.hack_set_program(&ctx, program).await.unwrap();
12502        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12503        let sketch_id = sketch_object.id;
12504        let sketch = expect_sketch(sketch_object);
12505
12506        // Extract segment IDs before edit_sketch borrows frontend mutably.
12507        let line_id = sketch
12508            .segments
12509            .iter()
12510            .copied()
12511            .find(|seg_id| {
12512                matches!(
12513                    &frontend.scene_graph.objects[seg_id.0].kind,
12514                    ObjectKind::Segment {
12515                        segment: Segment::Line(_)
12516                    }
12517                )
12518            })
12519            .expect("Expected a line segment in sketch");
12520
12521        // Enter sketch edit mode.
12522        frontend
12523            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
12524            .await
12525            .unwrap();
12526
12527        // Edit the line.
12528        let line_ctor = LineCtor {
12529            start: Point2d {
12530                x: Expr::Var(Number {
12531                    value: 1.0,
12532                    units: NumericSuffix::Mm,
12533                }),
12534                y: Expr::Var(Number {
12535                    value: 2.0,
12536                    units: NumericSuffix::Mm,
12537                }),
12538            },
12539            end: Point2d {
12540                x: Expr::Var(Number {
12541                    value: 13.0,
12542                    units: NumericSuffix::Mm,
12543                }),
12544                y: Expr::Var(Number {
12545                    value: 14.0,
12546                    units: NumericSuffix::Mm,
12547                }),
12548            },
12549            construction: None,
12550        };
12551        let segments = vec![ExistingSegmentCtor {
12552            id: line_id,
12553            ctor: SegmentCtor::Line(line_ctor),
12554        }];
12555        let (src_delta, _scene_delta) = frontend
12556            .edit_segments(&mock_ctx, version, sketch_id, segments)
12557            .await
12558            .unwrap();
12559        assert!(
12560            src_delta
12561                .text
12562                .contains("line(start = [var 1mm, var 2mm], end = [var 13mm, var 14mm])"),
12563            "Expected edited line in source, got: {}",
12564            src_delta.text
12565        );
12566
12567        ctx.close().await;
12568        mock_ctx.close().await;
12569    }
12570
12571    #[tokio::test(flavor = "multi_thread")]
12572    async fn test_extra_newlines_delete_segment() {
12573        // Extra whitespace before and after the sketch block.
12574        let initial_source = "@settings(defaultLengthUnit = mm)
12575
12576
12577
12578sketch001 = sketch(on = XY) {
12579  circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
12580}
12581";
12582
12583        let program = Program::parse(initial_source).unwrap().0.unwrap();
12584        let mut frontend = FrontendState::new();
12585
12586        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12587        let mock_ctx = ExecutorContext::new_mock(None).await;
12588        let version = Version(0);
12589
12590        frontend.hack_set_program(&ctx, program).await.unwrap();
12591        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12592        let sketch_id = sketch_object.id;
12593        let sketch = expect_sketch(sketch_object);
12594
12595        // The sketch should have 3 segments: start point, center point, and the circle.
12596        assert_eq!(sketch.segments.len(), 3);
12597        let circle_id = sketch.segments[2];
12598
12599        // Delete the circle despite extra newlines in original source.
12600        let (src_delta, scene_delta) = frontend
12601            .delete_objects(&mock_ctx, version, sketch_id, vec![], vec![circle_id])
12602            .await
12603            .unwrap();
12604        assert!(
12605            src_delta.text.contains("sketch(on = XY) {"),
12606            "Expected sketch block in source, got: {}",
12607            src_delta.text
12608        );
12609        let new_sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
12610        let new_sketch = expect_sketch(new_sketch_object);
12611        assert_eq!(new_sketch.segments.len(), 0);
12612
12613        ctx.close().await;
12614        mock_ctx.close().await;
12615    }
12616
12617    #[tokio::test(flavor = "multi_thread")]
12618    async fn test_unformatted_source_add_arc() {
12619        // Source with inconsistent whitespace - tabs, extra spaces, multiple blank lines.
12620        let initial_source = "@settings(defaultLengthUnit = mm)
12621
12622
12623
12624
12625sketch001 = sketch(on = XY) {
12626}
12627";
12628
12629        let program = Program::parse(initial_source).unwrap().0.unwrap();
12630        let mut frontend = FrontendState::new();
12631
12632        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12633        let mock_ctx = ExecutorContext::new_mock(None).await;
12634        let version = Version(0);
12635
12636        frontend.hack_set_program(&ctx, program).await.unwrap();
12637        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12638        let sketch_id = sketch_object.id;
12639
12640        let arc_ctor = ArcCtor {
12641            start: Point2d {
12642                x: Expr::Var(Number {
12643                    value: 5.0,
12644                    units: NumericSuffix::Mm,
12645                }),
12646                y: Expr::Var(Number {
12647                    value: 0.0,
12648                    units: NumericSuffix::Mm,
12649                }),
12650            },
12651            end: Point2d {
12652                x: Expr::Var(Number {
12653                    value: 0.0,
12654                    units: NumericSuffix::Mm,
12655                }),
12656                y: Expr::Var(Number {
12657                    value: 5.0,
12658                    units: NumericSuffix::Mm,
12659                }),
12660            },
12661            center: Point2d {
12662                x: Expr::Var(Number {
12663                    value: 0.0,
12664                    units: NumericSuffix::Mm,
12665                }),
12666                y: Expr::Var(Number {
12667                    value: 0.0,
12668                    units: NumericSuffix::Mm,
12669                }),
12670            },
12671            construction: None,
12672        };
12673        let segment = SegmentCtor::Arc(arc_ctor);
12674        let (src_delta, scene_delta) = frontend
12675            .add_segment(&mock_ctx, version, sketch_id, segment, None)
12676            .await
12677            .unwrap();
12678        assert!(
12679            src_delta
12680                .text
12681                .contains("arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])"),
12682            "Expected arc in source, got: {}",
12683            src_delta.text
12684        );
12685        assert!(!scene_delta.new_objects.is_empty());
12686
12687        ctx.close().await;
12688        mock_ctx.close().await;
12689    }
12690
12691    #[tokio::test(flavor = "multi_thread")]
12692    async fn test_extra_newlines_add_circle() {
12693        // Extra blank lines between settings and sketch.
12694        let initial_source = "@settings(defaultLengthUnit = mm)
12695
12696
12697
12698sketch001 = sketch(on = XY) {
12699}
12700";
12701
12702        let program = Program::parse(initial_source).unwrap().0.unwrap();
12703        let mut frontend = FrontendState::new();
12704
12705        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12706        let mock_ctx = ExecutorContext::new_mock(None).await;
12707        let version = Version(0);
12708
12709        frontend.hack_set_program(&ctx, program).await.unwrap();
12710        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12711        let sketch_id = sketch_object.id;
12712
12713        let circle_ctor = CircleCtor {
12714            start: Point2d {
12715                x: Expr::Var(Number {
12716                    value: 5.0,
12717                    units: NumericSuffix::Mm,
12718                }),
12719                y: Expr::Var(Number {
12720                    value: 0.0,
12721                    units: NumericSuffix::Mm,
12722                }),
12723            },
12724            center: Point2d {
12725                x: Expr::Var(Number {
12726                    value: 0.0,
12727                    units: NumericSuffix::Mm,
12728                }),
12729                y: Expr::Var(Number {
12730                    value: 0.0,
12731                    units: NumericSuffix::Mm,
12732                }),
12733            },
12734            construction: None,
12735        };
12736        let segment = SegmentCtor::Circle(circle_ctor);
12737        let (src_delta, scene_delta) = frontend
12738            .add_segment(&mock_ctx, version, sketch_id, segment, None)
12739            .await
12740            .unwrap();
12741        assert!(
12742            src_delta
12743                .text
12744                .contains("circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])"),
12745            "Expected circle in source, got: {}",
12746            src_delta.text
12747        );
12748        assert!(!scene_delta.new_objects.is_empty());
12749
12750        ctx.close().await;
12751        mock_ctx.close().await;
12752    }
12753
12754    #[tokio::test(flavor = "multi_thread")]
12755    async fn test_extra_newlines_add_constraint() {
12756        // Extra newlines with a sketch containing two lines - add a coincident constraint.
12757        let initial_source = "@settings(defaultLengthUnit = mm)
12758
12759
12760
12761sketch001 = sketch(on = XY) {
12762  line1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm])
12763  line2 = line(start = [var 10mm, var 10mm], end = [var 20mm, var 0mm])
12764}
12765";
12766
12767        let program = Program::parse(initial_source).unwrap().0.unwrap();
12768        let mut frontend = FrontendState::new();
12769
12770        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12771        let mock_ctx = ExecutorContext::new_mock(None).await;
12772        let version = Version(0);
12773        let project_id = ProjectId(0);
12774        let file_id = FileId(0);
12775
12776        frontend.hack_set_program(&ctx, program).await.unwrap();
12777        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12778        let sketch_id = sketch_object.id;
12779        let sketch = expect_sketch(sketch_object);
12780
12781        // Extract segment data before edit_sketch borrows frontend mutably.
12782        let line_ids: Vec<ObjectId> = sketch
12783            .segments
12784            .iter()
12785            .copied()
12786            .filter(|seg_id| {
12787                matches!(
12788                    &frontend.scene_graph.objects[seg_id.0].kind,
12789                    ObjectKind::Segment {
12790                        segment: Segment::Line(_)
12791                    }
12792                )
12793            })
12794            .collect();
12795        assert_eq!(line_ids.len(), 2, "Expected two line segments");
12796
12797        let line1 = &frontend.scene_graph.objects[line_ids[0].0];
12798        let ObjectKind::Segment {
12799            segment: Segment::Line(line1_data),
12800        } = &line1.kind
12801        else {
12802            panic!("Expected line");
12803        };
12804        let line2 = &frontend.scene_graph.objects[line_ids[1].0];
12805        let ObjectKind::Segment {
12806            segment: Segment::Line(line2_data),
12807        } = &line2.kind
12808        else {
12809            panic!("Expected line");
12810        };
12811
12812        // Build constraint before entering sketch mode.
12813        let constraint = Constraint::Coincident(Coincident {
12814            segments: vec![line1_data.end.into(), line2_data.start.into()],
12815        });
12816
12817        // Enter sketch edit mode.
12818        frontend
12819            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
12820            .await
12821            .unwrap();
12822        let (src_delta, _scene_delta) = frontend
12823            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12824            .await
12825            .unwrap();
12826        assert!(
12827            src_delta.text.contains("coincident("),
12828            "Expected coincident constraint in source, got: {}",
12829            src_delta.text
12830        );
12831
12832        ctx.close().await;
12833        mock_ctx.close().await;
12834    }
12835
12836    #[tokio::test(flavor = "multi_thread")]
12837    async fn test_extra_newlines_add_line_then_edit_line() {
12838        // Extra newlines after @settings - add a line, then edit it.
12839        let initial_source = "@settings(defaultLengthUnit = mm)
12840
12841
12842
12843sketch001 = sketch(on = XY) {
12844}
12845";
12846
12847        let program = Program::parse(initial_source).unwrap().0.unwrap();
12848        let mut frontend = FrontendState::new();
12849
12850        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12851        let mock_ctx = ExecutorContext::new_mock(None).await;
12852        let version = Version(0);
12853
12854        frontend.hack_set_program(&ctx, program).await.unwrap();
12855        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12856        let sketch_id = sketch_object.id;
12857
12858        // Add a line.
12859        let line_ctor = LineCtor {
12860            start: Point2d {
12861                x: Expr::Number(Number {
12862                    value: 0.0,
12863                    units: NumericSuffix::Mm,
12864                }),
12865                y: Expr::Number(Number {
12866                    value: 0.0,
12867                    units: NumericSuffix::Mm,
12868                }),
12869            },
12870            end: Point2d {
12871                x: Expr::Number(Number {
12872                    value: 10.0,
12873                    units: NumericSuffix::Mm,
12874                }),
12875                y: Expr::Number(Number {
12876                    value: 10.0,
12877                    units: NumericSuffix::Mm,
12878                }),
12879            },
12880            construction: None,
12881        };
12882        let segment = SegmentCtor::Line(line_ctor);
12883        let (src_delta, scene_delta) = frontend
12884            .add_segment(&mock_ctx, version, sketch_id, segment, None)
12885            .await
12886            .unwrap();
12887        assert!(
12888            src_delta.text.contains("line(start = [0mm, 0mm], end = [10mm, 10mm])"),
12889            "Expected line in source after add, got: {}",
12890            src_delta.text
12891        );
12892        // Line creates start point, end point, and line segment.
12893        let line_id = *scene_delta.new_objects.last().unwrap();
12894
12895        // Edit the line.
12896        let line_ctor = LineCtor {
12897            start: Point2d {
12898                x: Expr::Number(Number {
12899                    value: 1.0,
12900                    units: NumericSuffix::Mm,
12901                }),
12902                y: Expr::Number(Number {
12903                    value: 2.0,
12904                    units: NumericSuffix::Mm,
12905                }),
12906            },
12907            end: Point2d {
12908                x: Expr::Number(Number {
12909                    value: 13.0,
12910                    units: NumericSuffix::Mm,
12911                }),
12912                y: Expr::Number(Number {
12913                    value: 14.0,
12914                    units: NumericSuffix::Mm,
12915                }),
12916            },
12917            construction: None,
12918        };
12919        let segments = vec![ExistingSegmentCtor {
12920            id: line_id,
12921            ctor: SegmentCtor::Line(line_ctor),
12922        }];
12923        let (src_delta, scene_delta) = frontend
12924            .edit_segments(&mock_ctx, version, sketch_id, segments)
12925            .await
12926            .unwrap();
12927        assert!(
12928            src_delta.text.contains("line(start = [1mm, 2mm], end = [13mm, 14mm])"),
12929            "Expected edited line in source, got: {}",
12930            src_delta.text
12931        );
12932        assert_eq!(scene_delta.new_objects, vec![]);
12933
12934        ctx.close().await;
12935        mock_ctx.close().await;
12936    }
12937}