Skip to main content

kcl_lib/
frontend.rs

1use std::cell::Cell;
2use std::collections::HashMap;
3use std::collections::HashSet;
4use std::collections::VecDeque;
5use std::ops::ControlFlow;
6
7use indexmap::IndexMap;
8use kcl_error::CompilationIssue;
9use kcl_error::SourceRange;
10use kittycad_modeling_cmds::units::UnitLength;
11use serde::Serialize;
12
13use crate::ExecOutcome;
14use crate::ExecutorContext;
15use crate::KclError;
16use crate::KclErrorWithOutputs;
17use crate::Program;
18use crate::collections::AhashIndexSet;
19#[cfg(feature = "artifact-graph")]
20use crate::execution::Artifact;
21#[cfg(feature = "artifact-graph")]
22use crate::execution::ArtifactGraph;
23#[cfg(feature = "artifact-graph")]
24use crate::execution::CapSubType;
25use crate::execution::MockConfig;
26use crate::execution::SKETCH_BLOCK_PARAM_ON;
27use crate::execution::cache::SketchModeState;
28use crate::execution::cache::clear_mem_cache;
29use crate::execution::cache::read_old_memory;
30use crate::execution::cache::write_old_memory;
31use crate::fmt::format_number_literal;
32use crate::front::Angle;
33use crate::front::ArcCtor;
34use crate::front::CircleCtor;
35use crate::front::Distance;
36use crate::front::EqualRadius;
37use crate::front::Error;
38use crate::front::ExecResult;
39use crate::front::FixedPoint;
40use crate::front::Freedom;
41use crate::front::LinesEqualLength;
42use crate::front::Midpoint;
43use crate::front::Object;
44use crate::front::Parallel;
45use crate::front::Perpendicular;
46use crate::front::PointCtor;
47use crate::front::Tangent;
48use crate::frontend::api::Expr;
49use crate::frontend::api::FileId;
50use crate::frontend::api::Number;
51use crate::frontend::api::ObjectId;
52use crate::frontend::api::ObjectKind;
53use crate::frontend::api::Plane;
54use crate::frontend::api::ProjectId;
55use crate::frontend::api::RestoreSketchCheckpointOutcome;
56use crate::frontend::api::SceneGraph;
57use crate::frontend::api::SceneGraphDelta;
58use crate::frontend::api::SketchCheckpointId;
59use crate::frontend::api::SourceDelta;
60use crate::frontend::api::SourceRef;
61use crate::frontend::api::Version;
62use crate::frontend::modify::find_defined_names;
63use crate::frontend::modify::next_free_name;
64use crate::frontend::modify::next_free_name_with_padding;
65use crate::frontend::sketch::Coincident;
66use crate::frontend::sketch::Constraint;
67use crate::frontend::sketch::ConstraintSegment;
68use crate::frontend::sketch::Diameter;
69use crate::frontend::sketch::ExistingSegmentCtor;
70use crate::frontend::sketch::Horizontal;
71use crate::frontend::sketch::LineCtor;
72use crate::frontend::sketch::Point2d;
73use crate::frontend::sketch::Radius;
74use crate::frontend::sketch::Segment;
75use crate::frontend::sketch::SegmentCtor;
76use crate::frontend::sketch::SketchApi;
77use crate::frontend::sketch::SketchCtor;
78use crate::frontend::sketch::Vertical;
79use crate::frontend::traverse::MutateBodyItem;
80use crate::frontend::traverse::TraversalReturn;
81use crate::frontend::traverse::Visitor;
82use crate::frontend::traverse::dfs_mut;
83use crate::id::IncIdGenerator;
84use crate::parsing::ast::types as ast;
85use crate::pretty::NumericSuffix;
86use crate::std::constraints::LinesAtAngleKind;
87use crate::walk::NodeMut;
88use crate::walk::Visitable;
89
90pub(crate) mod api;
91pub(crate) mod modify;
92pub(crate) mod sketch;
93
94pub const MAX_SKETCH_CHECKPOINTS: usize = 100;
95
96#[derive(Debug, Clone)]
97struct SketchCheckpoint {
98    id: SketchCheckpointId,
99    source: SourceDelta,
100    program: Program,
101    scene_graph: SceneGraph,
102    exec_outcome: ExecOutcome,
103    point_freedom_cache: HashMap<ObjectId, Freedom>,
104    mock_memory: Option<SketchModeState>,
105}
106mod traverse;
107pub(crate) mod trim;
108
109struct ArcSizeConstraintParams {
110    points: Vec<ObjectId>,
111    function_name: &'static str,
112    value: f64,
113    units: NumericSuffix,
114    constraint_type_name: &'static str,
115}
116
117const POINT_FN: &str = "point";
118const POINT_AT_PARAM: &str = "at";
119const LINE_FN: &str = "line";
120const LINE_START_PARAM: &str = "start";
121const LINE_END_PARAM: &str = "end";
122const ARC_FN: &str = "arc";
123const ARC_START_PARAM: &str = "start";
124const ARC_END_PARAM: &str = "end";
125const ARC_CENTER_PARAM: &str = "center";
126const CIRCLE_FN: &str = "circle";
127const CIRCLE_VARIABLE: &str = "circle";
128const CIRCLE_START_PARAM: &str = "start";
129const CIRCLE_CENTER_PARAM: &str = "center";
130
131const COINCIDENT_FN: &str = "coincident";
132const DIAMETER_FN: &str = "diameter";
133const DISTANCE_FN: &str = "distance";
134const FIXED_FN: &str = "fixed";
135const ANGLE_FN: &str = "angle";
136const HORIZONTAL_DISTANCE_FN: &str = "horizontalDistance";
137const VERTICAL_DISTANCE_FN: &str = "verticalDistance";
138const EQUAL_LENGTH_FN: &str = "equalLength";
139const EQUAL_RADIUS_FN: &str = "equalRadius";
140const HORIZONTAL_FN: &str = "horizontal";
141const MIDPOINT_FN: &str = "midpoint";
142const MIDPOINT_POINT_PARAM: &str = "point";
143const RADIUS_FN: &str = "radius";
144const TANGENT_FN: &str = "tangent";
145const VERTICAL_FN: &str = "vertical";
146
147const LINE_PROPERTY_START: &str = "start";
148const LINE_PROPERTY_END: &str = "end";
149
150const ARC_PROPERTY_START: &str = "start";
151const ARC_PROPERTY_END: &str = "end";
152const ARC_PROPERTY_CENTER: &str = "center";
153const CIRCLE_PROPERTY_START: &str = "start";
154const CIRCLE_PROPERTY_CENTER: &str = "center";
155
156const CONSTRUCTION_PARAM: &str = "construction";
157
158#[derive(Debug, Clone, Copy)]
159enum EditDeleteKind {
160    Edit,
161    DeleteNonSketch,
162}
163
164impl EditDeleteKind {
165    /// Returns true if this edit is any type of deletion.
166    fn is_delete(&self) -> bool {
167        match self {
168            EditDeleteKind::Edit => false,
169            EditDeleteKind::DeleteNonSketch => true,
170        }
171    }
172
173    fn to_change_kind(self) -> ChangeKind {
174        match self {
175            EditDeleteKind::Edit => ChangeKind::Edit,
176            EditDeleteKind::DeleteNonSketch => ChangeKind::Delete,
177        }
178    }
179}
180
181#[derive(Debug, Clone, Copy)]
182enum ChangeKind {
183    Add,
184    Edit,
185    Delete,
186    None,
187}
188
189#[derive(Debug, Clone, Serialize, ts_rs::TS)]
190#[ts(export, export_to = "FrontendApi.ts")]
191#[serde(tag = "type")]
192pub enum SetProgramOutcome {
193    #[serde(rename_all = "camelCase")]
194    Success {
195        scene_graph: Box<SceneGraph>,
196        exec_outcome: Box<ExecOutcome>,
197        checkpoint_id: Option<SketchCheckpointId>,
198    },
199    #[serde(rename_all = "camelCase")]
200    ExecFailure { error: Box<KclErrorWithOutputs> },
201}
202
203#[derive(Debug, Clone)]
204pub struct FrontendState {
205    program: Program,
206    scene_graph: SceneGraph,
207    /// Stores the last known freedom value for each point object.
208    /// This allows us to preserve freedom values when freedom analysis isn't run.
209    point_freedom_cache: HashMap<ObjectId, Freedom>,
210    sketch_checkpoints: VecDeque<SketchCheckpoint>,
211    sketch_checkpoint_id_gen: IncIdGenerator<u64>,
212}
213
214impl Default for FrontendState {
215    fn default() -> Self {
216        Self::new()
217    }
218}
219
220impl FrontendState {
221    pub fn new() -> Self {
222        Self {
223            program: Program::empty(),
224            scene_graph: SceneGraph {
225                project: ProjectId(0),
226                file: FileId(0),
227                version: Version(0),
228                objects: Default::default(),
229                settings: Default::default(),
230                sketch_mode: Default::default(),
231            },
232            point_freedom_cache: HashMap::new(),
233            sketch_checkpoints: VecDeque::new(),
234            sketch_checkpoint_id_gen: IncIdGenerator::new(1),
235        }
236    }
237
238    /// Get a reference to the scene graph
239    pub fn scene_graph(&self) -> &SceneGraph {
240        &self.scene_graph
241    }
242
243    pub fn default_length_unit(&self) -> UnitLength {
244        self.program
245            .meta_settings()
246            .ok()
247            .flatten()
248            .map(|settings| settings.default_length_units)
249            .unwrap_or(UnitLength::Millimeters)
250    }
251
252    pub async fn create_sketch_checkpoint(&mut self, exec_outcome: ExecOutcome) -> api::Result<SketchCheckpointId> {
253        let checkpoint_id = SketchCheckpointId::new(self.sketch_checkpoint_id_gen.next_id());
254
255        let checkpoint = SketchCheckpoint {
256            id: checkpoint_id,
257            source: SourceDelta {
258                text: source_from_ast(&self.program.ast),
259            },
260            program: self.program.clone(),
261            scene_graph: self.scene_graph.clone(),
262            exec_outcome,
263            point_freedom_cache: self.point_freedom_cache.clone(),
264            mock_memory: read_old_memory().await,
265        };
266
267        self.sketch_checkpoints.push_back(checkpoint);
268        while self.sketch_checkpoints.len() > MAX_SKETCH_CHECKPOINTS {
269            self.sketch_checkpoints.pop_front();
270        }
271
272        Ok(checkpoint_id)
273    }
274
275    pub async fn restore_sketch_checkpoint(
276        &mut self,
277        checkpoint_id: SketchCheckpointId,
278    ) -> api::Result<RestoreSketchCheckpointOutcome> {
279        let checkpoint = self
280            .sketch_checkpoints
281            .iter()
282            .find(|checkpoint| checkpoint.id == checkpoint_id)
283            .cloned()
284            .ok_or_else(|| Error {
285                msg: format!("Sketch checkpoint not found: {checkpoint_id:?}"),
286            })?;
287
288        self.program = checkpoint.program;
289        self.scene_graph = checkpoint.scene_graph.clone();
290        self.point_freedom_cache = checkpoint.point_freedom_cache;
291
292        if let Some(mock_memory) = checkpoint.mock_memory {
293            write_old_memory(mock_memory).await;
294        } else {
295            clear_mem_cache().await;
296        }
297
298        Ok(RestoreSketchCheckpointOutcome {
299            source_delta: checkpoint.source,
300            scene_graph_delta: SceneGraphDelta {
301                new_graph: checkpoint.scene_graph,
302                new_objects: Vec::new(),
303                invalidates_ids: true,
304                exec_outcome: checkpoint.exec_outcome,
305            },
306        })
307    }
308
309    pub fn clear_sketch_checkpoints(&mut self) {
310        self.sketch_checkpoints.clear();
311    }
312}
313
314impl SketchApi for FrontendState {
315    async fn execute_mock(
316        &mut self,
317        ctx: &ExecutorContext,
318        _version: Version,
319        sketch: ObjectId,
320    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
321        let sketch_block_ref =
322            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
323
324        let mut truncated_program = self.program.clone();
325        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::None)
326            .map_err(KclErrorWithOutputs::no_outputs)?;
327
328        // Execute.
329        let outcome = ctx
330            .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch))
331            .await?;
332        let new_source = source_from_ast(&self.program.ast);
333        let src_delta = SourceDelta { text: new_source };
334        // MockConfig::default() has freedom_analysis: true
335        let outcome = self.update_state_after_exec(outcome, true);
336        let scene_graph_delta = SceneGraphDelta {
337            new_graph: self.scene_graph.clone(),
338            new_objects: Default::default(),
339            invalidates_ids: false,
340            exec_outcome: outcome,
341        };
342        Ok((src_delta, scene_graph_delta))
343    }
344
345    async fn new_sketch(
346        &mut self,
347        ctx: &ExecutorContext,
348        _project: ProjectId,
349        _file: FileId,
350        _version: Version,
351        args: SketchCtor,
352    ) -> ExecResult<(SourceDelta, SceneGraphDelta, ObjectId)> {
353        // TODO: Check version.
354
355        let mut new_ast = self.program.ast.clone();
356        // Create updated KCL source from args.
357        let mut plane_ast =
358            sketch_on_ast_expr(&mut new_ast, &self.scene_graph, &args.on).map_err(KclErrorWithOutputs::no_outputs)?;
359        let mut defined_names = find_defined_names(&new_ast);
360        let is_face_of_expr = matches!(
361            &plane_ast,
362            ast::Expr::CallExpressionKw(call) if call.callee.name.name == "faceOf"
363        );
364        if is_face_of_expr {
365            let face_name = next_free_name_with_padding("face", &defined_names)
366                .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
367            let face_decl = ast::VariableDeclaration::new(
368                ast::VariableDeclarator::new(&face_name, plane_ast),
369                ast::ItemVisibility::Default,
370                ast::VariableKind::Const,
371            );
372            new_ast
373                .body
374                .push(ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(
375                    face_decl,
376                ))));
377            defined_names.insert(face_name.clone());
378            plane_ast = ast::Expr::Name(Box::new(ast::Name::new(&face_name)));
379        }
380        let sketch_ast = ast::SketchBlock {
381            arguments: vec![ast::LabeledArg {
382                label: Some(ast::Identifier::new(SKETCH_BLOCK_PARAM_ON)),
383                arg: plane_ast,
384            }],
385            body: Default::default(),
386            is_being_edited: false,
387            non_code_meta: Default::default(),
388            digest: None,
389        };
390        // Add a sketch block as a variable declaration directly, avoiding
391        // source-range mutation on a no-src node.
392        let sketch_name = next_free_name_with_padding("sketch", &defined_names)
393            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
394        let sketch_decl = ast::VariableDeclaration::new(
395            ast::VariableDeclarator::new(
396                &sketch_name,
397                ast::Expr::SketchBlock(Box::new(ast::Node::no_src(sketch_ast))),
398            ),
399            ast::ItemVisibility::Default,
400            ast::VariableKind::Const,
401        );
402        new_ast
403            .body
404            .push(ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(
405                sketch_decl,
406            ))));
407        // Convert to string source to create real source ranges.
408        let new_source = source_from_ast(&new_ast);
409        // Parse the new source.
410        let (new_program, errors) = Program::parse(&new_source)
411            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
412        if !errors.is_empty() {
413            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
414                "Error parsing KCL source after adding sketch: {errors:?}"
415            ))));
416        }
417        let Some(new_program) = new_program else {
418            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
419                "No AST produced after adding sketch".to_owned(),
420            )));
421        };
422
423        // Make sure to only set this if there are no errors.
424        self.program = new_program.clone();
425
426        // We need to do an engine execute so that the plane object gets created
427        // and is cached.
428        let outcome = ctx.run_with_caching(new_program.clone()).await?;
429        let freedom_analysis_ran = true;
430
431        let outcome = self.update_state_after_exec(outcome, freedom_analysis_ran);
432
433        let Some(sketch_id) = self
434            .scene_graph
435            .objects
436            .iter()
437            .filter_map(|object| match object.kind {
438                ObjectKind::Sketch(_) => Some(object.id),
439                _ => None,
440            })
441            .max_by_key(|id| id.0)
442        else {
443            return Err(KclErrorWithOutputs::from_error_outcome(
444                KclError::refactor("No objects in scene graph after adding sketch".to_owned()),
445                outcome,
446            ));
447        };
448        // Store the object in the scene.
449        self.scene_graph.sketch_mode = Some(sketch_id);
450
451        let src_delta = SourceDelta { text: new_source };
452        let scene_graph_delta = SceneGraphDelta {
453            new_graph: self.scene_graph.clone(),
454            invalidates_ids: false,
455            new_objects: vec![sketch_id],
456            exec_outcome: outcome,
457        };
458        Ok((src_delta, scene_graph_delta, sketch_id))
459    }
460
461    async fn edit_sketch(
462        &mut self,
463        ctx: &ExecutorContext,
464        _project: ProjectId,
465        _file: FileId,
466        _version: Version,
467        sketch: ObjectId,
468    ) -> ExecResult<SceneGraphDelta> {
469        // TODO: Check version.
470
471        // Look up existing sketch.
472        let sketch_object = self.scene_graph.objects.get(sketch.0).ok_or_else(|| {
473            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
474        })?;
475        let ObjectKind::Sketch(_) = &sketch_object.kind else {
476            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
477                "Object is not a sketch, it is {}",
478                sketch_object.kind.human_friendly_kind_with_article()
479            ))));
480        };
481        let sketch_block_ref = expect_single_node_ref(sketch_object).map_err(KclErrorWithOutputs::no_outputs)?;
482
483        // Enter sketch mode by setting the sketch_mode.
484        self.scene_graph.sketch_mode = Some(sketch);
485
486        // Truncate after the sketch block for mock execution.
487        let mut truncated_program = self.program.clone();
488        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::None)
489            .map_err(KclErrorWithOutputs::no_outputs)?;
490
491        // Execute in mock mode to ensure state is up to date. The caller will
492        // want freedom analysis to display segments correctly.
493        let outcome = ctx
494            .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch))
495            .await?;
496
497        // MockConfig::default() has freedom_analysis: true
498        let outcome = self.update_state_after_exec(outcome, true);
499        let scene_graph_delta = SceneGraphDelta {
500            new_graph: self.scene_graph.clone(),
501            invalidates_ids: false,
502            new_objects: Vec::new(),
503            exec_outcome: outcome,
504        };
505        Ok(scene_graph_delta)
506    }
507
508    async fn exit_sketch(
509        &mut self,
510        ctx: &ExecutorContext,
511        _version: Version,
512        sketch: ObjectId,
513    ) -> ExecResult<SceneGraph> {
514        // TODO: Check version.
515        #[cfg(not(target_arch = "wasm32"))]
516        let _ = sketch;
517        #[cfg(target_arch = "wasm32")]
518        if self.scene_graph.sketch_mode != Some(sketch) {
519            web_sys::console::warn_1(
520                &format!(
521                    "WARNING: exit_sketch: current state's sketch mode ID doesn't match the given sketch ID; state={:#?}, given={sketch:?}",
522                    &self.scene_graph.sketch_mode
523                )
524                .into(),
525            );
526        }
527        self.scene_graph.sketch_mode = None;
528
529        // Execute.
530        let outcome = ctx.run_with_caching(self.program.clone()).await?;
531
532        // exit_sketch doesn't run freedom analysis, just clears sketch_mode
533        self.update_state_after_exec(outcome, false);
534
535        Ok(self.scene_graph.clone())
536    }
537
538    async fn delete_sketch(
539        &mut self,
540        ctx: &ExecutorContext,
541        _version: Version,
542        sketch: ObjectId,
543    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
544        // TODO: Check version.
545
546        let mut new_ast = self.program.ast.clone();
547
548        // Look up existing sketch.
549        let sketch_id = sketch;
550        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
551            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
552        })?;
553        let ObjectKind::Sketch(_) = &sketch_object.kind else {
554            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
555                "Object is not a sketch, it is {}",
556                sketch_object.kind.human_friendly_kind_with_article(),
557            ))));
558        };
559
560        // Modify the AST to remove the sketch.
561        self.mutate_ast(&mut new_ast, sketch_id, AstMutateCommand::DeleteNode)
562            .map_err(KclErrorWithOutputs::no_outputs)?;
563
564        self.execute_after_delete_sketch(ctx, &mut new_ast).await
565    }
566
567    async fn add_segment(
568        &mut self,
569        ctx: &ExecutorContext,
570        _version: Version,
571        sketch: ObjectId,
572        segment: SegmentCtor,
573        _label: Option<String>,
574    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
575        // TODO: Check version.
576        match segment {
577            SegmentCtor::Point(ctor) => self.add_point(ctx, sketch, ctor).await,
578            SegmentCtor::Line(ctor) => self.add_line(ctx, sketch, ctor).await,
579            SegmentCtor::Arc(ctor) => self.add_arc(ctx, sketch, ctor).await,
580            SegmentCtor::Circle(ctor) => self.add_circle(ctx, sketch, ctor).await,
581        }
582    }
583
584    async fn edit_segments(
585        &mut self,
586        ctx: &ExecutorContext,
587        _version: Version,
588        sketch: ObjectId,
589        segments: Vec<ExistingSegmentCtor>,
590    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
591        // TODO: Check version.
592        let sketch_block_ref =
593            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
594
595        let mut new_ast = self.program.ast.clone();
596        let mut segment_ids_edited = AhashIndexSet::with_capacity_and_hasher(segments.len(), Default::default());
597
598        // segment_ids_edited still has to be the original segments (not final_edits), otherwise the owner segments
599        // are passed to `execute_after_edit` which changes the result of the solver, causing tests to fail.
600        for segment in &segments {
601            segment_ids_edited.insert(segment.id);
602        }
603
604        // Preprocess segments into a final_edits vector to handle if segments contains:
605        // - edit start point of line1 (as SegmentCtor::Point)
606        // - edit end point of line1 (as SegmentCtor::Point)
607        //
608        // This would result in only the end point to be updated because edit_point() clones line1's ctor from
609        // scene_graph, but this is still the old ctor because self.scene_graph is only updated after the loop finishes.
610        //
611        // To fix this, and other cases when the same point is edited from multiple elements in the segments Vec
612        // we apply all edits in order to final_edits in a way that owned point edits result in line edits,
613        // so the above example would result in a single line1 edit:
614        // - the first start point edit creates a new line edit entry in final_edits
615        // - the second end point edit finds this line edit and mutates the end position only.
616        //
617        // The result is that segments are flattened into a single IndexMap of edits by their owners, later edits overriding earlier ones.
618        let mut final_edits: IndexMap<ObjectId, SegmentCtor> = IndexMap::new();
619
620        for segment in segments {
621            let segment_id = segment.id;
622            match segment.ctor {
623                SegmentCtor::Point(ctor) => {
624                    // Find the owner, if any (point -> line / arc)
625                    if let Some(segment_object) = self.scene_graph.objects.get(segment_id.0)
626                        && let ObjectKind::Segment { segment } = &segment_object.kind
627                        && let Segment::Point(point) = segment
628                        && let Some(owner_id) = point.owner
629                        && let Some(owner_object) = self.scene_graph.objects.get(owner_id.0)
630                        && let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind
631                    {
632                        match owner_segment {
633                            Segment::Line(line) if line.start == segment_id || line.end == segment_id => {
634                                if let Some(existing) = final_edits.get_mut(&owner_id) {
635                                    let SegmentCtor::Line(line_ctor) = existing else {
636                                        return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
637                                            "Internal: Expected line ctor for owner, but found {}",
638                                            existing.human_friendly_kind_with_article()
639                                        ))));
640                                    };
641                                    // Line owner is already in final_edits -> apply this point edit
642                                    if line.start == segment_id {
643                                        line_ctor.start = ctor.position;
644                                    } else {
645                                        line_ctor.end = ctor.position;
646                                    }
647                                } else if let SegmentCtor::Line(line_ctor) = &line.ctor {
648                                    // Line owner is not in final_edits yet -> create it
649                                    let mut line_ctor = line_ctor.clone();
650                                    if line.start == segment_id {
651                                        line_ctor.start = ctor.position;
652                                    } else {
653                                        line_ctor.end = ctor.position;
654                                    }
655                                    final_edits.insert(owner_id, SegmentCtor::Line(line_ctor));
656                                } else {
657                                    // This should never run..
658                                    return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
659                                        "Internal: Line does not have line ctor, but found {}",
660                                        line.ctor.human_friendly_kind_with_article()
661                                    ))));
662                                }
663                                continue;
664                            }
665                            Segment::Arc(arc)
666                                if arc.start == segment_id || arc.end == segment_id || arc.center == segment_id =>
667                            {
668                                if let Some(existing) = final_edits.get_mut(&owner_id) {
669                                    let SegmentCtor::Arc(arc_ctor) = existing else {
670                                        return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
671                                            "Internal: Expected arc ctor for owner, but found {}",
672                                            existing.human_friendly_kind_with_article()
673                                        ))));
674                                    };
675                                    if arc.start == segment_id {
676                                        arc_ctor.start = ctor.position;
677                                    } else if arc.end == segment_id {
678                                        arc_ctor.end = ctor.position;
679                                    } else {
680                                        arc_ctor.center = ctor.position;
681                                    }
682                                } else if let SegmentCtor::Arc(arc_ctor) = &arc.ctor {
683                                    let mut arc_ctor = arc_ctor.clone();
684                                    if arc.start == segment_id {
685                                        arc_ctor.start = ctor.position;
686                                    } else if arc.end == segment_id {
687                                        arc_ctor.end = ctor.position;
688                                    } else {
689                                        arc_ctor.center = ctor.position;
690                                    }
691                                    final_edits.insert(owner_id, SegmentCtor::Arc(arc_ctor));
692                                } else {
693                                    return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
694                                        "Internal: Arc does not have arc ctor, but found {}",
695                                        arc.ctor.human_friendly_kind_with_article()
696                                    ))));
697                                }
698                                continue;
699                            }
700                            Segment::Circle(circle) if circle.start == segment_id || circle.center == segment_id => {
701                                if let Some(existing) = final_edits.get_mut(&owner_id) {
702                                    let SegmentCtor::Circle(circle_ctor) = existing else {
703                                        return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
704                                            "Internal: Expected circle ctor for owner, but found {}",
705                                            existing.human_friendly_kind_with_article()
706                                        ))));
707                                    };
708                                    if circle.start == segment_id {
709                                        circle_ctor.start = ctor.position;
710                                    } else {
711                                        circle_ctor.center = ctor.position;
712                                    }
713                                } else if let SegmentCtor::Circle(circle_ctor) = &circle.ctor {
714                                    let mut circle_ctor = circle_ctor.clone();
715                                    if circle.start == segment_id {
716                                        circle_ctor.start = ctor.position;
717                                    } else {
718                                        circle_ctor.center = ctor.position;
719                                    }
720                                    final_edits.insert(owner_id, SegmentCtor::Circle(circle_ctor));
721                                } else {
722                                    return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
723                                        "Internal: Circle does not have circle ctor, but found {}",
724                                        circle.ctor.human_friendly_kind_with_article()
725                                    ))));
726                                }
727                                continue;
728                            }
729                            _ => {}
730                        }
731                    }
732
733                    // No owner, it's an individual point
734                    final_edits.insert(segment_id, SegmentCtor::Point(ctor));
735                }
736                SegmentCtor::Line(ctor) => {
737                    final_edits.insert(segment_id, SegmentCtor::Line(ctor));
738                }
739                SegmentCtor::Arc(ctor) => {
740                    final_edits.insert(segment_id, SegmentCtor::Arc(ctor));
741                }
742                SegmentCtor::Circle(ctor) => {
743                    final_edits.insert(segment_id, SegmentCtor::Circle(ctor));
744                }
745            }
746        }
747
748        for (segment_id, ctor) in final_edits {
749            match ctor {
750                SegmentCtor::Point(ctor) => self
751                    .edit_point(&mut new_ast, sketch, segment_id, ctor)
752                    .map_err(KclErrorWithOutputs::no_outputs)?,
753                SegmentCtor::Line(ctor) => self
754                    .edit_line(&mut new_ast, sketch, segment_id, ctor)
755                    .map_err(KclErrorWithOutputs::no_outputs)?,
756                SegmentCtor::Arc(ctor) => self
757                    .edit_arc(&mut new_ast, sketch, segment_id, ctor)
758                    .map_err(KclErrorWithOutputs::no_outputs)?,
759                SegmentCtor::Circle(ctor) => self
760                    .edit_circle(&mut new_ast, sketch, segment_id, ctor)
761                    .map_err(KclErrorWithOutputs::no_outputs)?,
762            }
763        }
764        self.execute_after_edit(
765            ctx,
766            sketch,
767            sketch_block_ref,
768            segment_ids_edited,
769            EditDeleteKind::Edit,
770            &mut new_ast,
771        )
772        .await
773    }
774
775    async fn delete_objects(
776        &mut self,
777        ctx: &ExecutorContext,
778        _version: Version,
779        sketch: ObjectId,
780        constraint_ids: Vec<ObjectId>,
781        segment_ids: Vec<ObjectId>,
782    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
783        // TODO: Check version.
784        let sketch_block_ref =
785            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
786
787        // Deduplicate IDs.
788        let mut constraint_ids_set = constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
789        let segment_ids_set = segment_ids.into_iter().collect::<AhashIndexSet<_>>();
790
791        // If a point is owned by a Line/Arc, we want to delete the owner, which will
792        // also delete the point, as well as other points that are owned by the owner.
793        let mut resolved_segment_ids_to_delete = AhashIndexSet::default();
794
795        for segment_id in segment_ids_set.iter().copied() {
796            if let Some(segment_object) = self.scene_graph.objects.get(segment_id.0)
797                && let ObjectKind::Segment { segment } = &segment_object.kind
798                && let Segment::Point(point) = segment
799                && let Some(owner_id) = point.owner
800                && let Some(owner_object) = self.scene_graph.objects.get(owner_id.0)
801                && let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind
802                && matches!(owner_segment, Segment::Line(_) | Segment::Arc(_) | Segment::Circle(_))
803            {
804                // segment is owned -> delete the owner
805                resolved_segment_ids_to_delete.insert(owner_id);
806            } else {
807                // segment is not owned by anything -> can be deleted
808                resolved_segment_ids_to_delete.insert(segment_id);
809            }
810        }
811        let referenced_constraint_ids = self
812            .find_referenced_constraints(sketch, &resolved_segment_ids_to_delete)
813            .map_err(KclErrorWithOutputs::no_outputs)?;
814
815        let mut new_ast = self.program.ast.clone();
816
817        for constraint_id in referenced_constraint_ids {
818            if constraint_ids_set.contains(&constraint_id) {
819                continue;
820            }
821
822            let constraint_object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
823                KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Constraint not found: {constraint_id:?}")))
824            })?;
825            let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
826                return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
827                    "Object is not a constraint, it is {}",
828                    constraint_object.kind.human_friendly_kind_with_article()
829                ))));
830            };
831
832            match constraint {
833                Constraint::Coincident(coincident) => {
834                    // The remaining segments are the segments that are not getting deleted
835                    let remaining_segments = coincident
836                        .segments
837                        .iter()
838                        .copied()
839                        .filter(|segment| match segment {
840                            ConstraintSegment::Segment(point_id) => {
841                                if resolved_segment_ids_to_delete.contains(point_id) {
842                                    // This point is getting deleted
843                                    return false;
844                                }
845                                let point_object = self.scene_graph.objects.get(point_id.0);
846                                if let Some(object) = point_object
847                                    && let ObjectKind::Segment { segment } = &object.kind
848                                    && let Segment::Point(point) = segment
849                                    && let Some(owner_id) = point.owner
850                                {
851                                    // If the owner of this point is getting deleted then the point is getting deleted too
852                                    // -> this point will not be in remaining_segments
853                                    return !resolved_segment_ids_to_delete.contains(&owner_id);
854                                }
855                                true
856                            }
857                            ConstraintSegment::Origin(_) => true,
858                        })
859                        .collect::<Vec<_>>();
860
861                    // If there are at least 2 segments left in the constraint: keep it, otherwise delete it.
862                    if remaining_segments.len() >= 2 {
863                        self.edit_coincident_constraint(&mut new_ast, constraint_id, remaining_segments)
864                            .map_err(KclErrorWithOutputs::no_outputs)?;
865                    } else {
866                        constraint_ids_set.insert(constraint_id);
867                    }
868                }
869                Constraint::EqualRadius(equal_radius) => {
870                    let remaining_input = equal_radius
871                        .input
872                        .iter()
873                        .copied()
874                        .filter(|segment_id| !resolved_segment_ids_to_delete.contains(segment_id))
875                        .collect::<Vec<_>>();
876
877                    if remaining_input.len() >= 2 {
878                        self.edit_equal_radius_constraint(&mut new_ast, constraint_id, remaining_input)
879                            .map_err(KclErrorWithOutputs::no_outputs)?;
880                    } else {
881                        constraint_ids_set.insert(constraint_id);
882                    }
883                }
884                Constraint::LinesEqualLength(lines_equal_length) => {
885                    let remaining_lines = lines_equal_length
886                        .lines
887                        .iter()
888                        .copied()
889                        .filter(|line_id| !resolved_segment_ids_to_delete.contains(line_id))
890                        .collect::<Vec<_>>();
891
892                    // Equal length constraint is only valid with at least 2 lines
893                    if remaining_lines.len() >= 2 {
894                        self.edit_equal_length_constraint(&mut new_ast, constraint_id, remaining_lines)
895                            .map_err(KclErrorWithOutputs::no_outputs)?;
896                    } else {
897                        constraint_ids_set.insert(constraint_id);
898                    }
899                }
900                Constraint::Parallel(parallel) => {
901                    let remaining_lines = parallel
902                        .lines
903                        .iter()
904                        .copied()
905                        .filter(|line_id| !resolved_segment_ids_to_delete.contains(line_id))
906                        .collect::<Vec<_>>();
907
908                    if remaining_lines.len() >= 2 {
909                        self.edit_parallel_constraint(&mut new_ast, constraint_id, remaining_lines)
910                            .map_err(KclErrorWithOutputs::no_outputs)?;
911                    } else {
912                        constraint_ids_set.insert(constraint_id);
913                    }
914                }
915                _ => {
916                    // All other constraint types: if referenced by a segment -> delete the constraint
917                    constraint_ids_set.insert(constraint_id);
918                }
919            }
920        }
921
922        for constraint_id in constraint_ids_set {
923            self.delete_constraint(&mut new_ast, sketch, constraint_id)
924                .map_err(KclErrorWithOutputs::no_outputs)?;
925        }
926        for segment_id in resolved_segment_ids_to_delete {
927            self.delete_segment(&mut new_ast, sketch, segment_id)
928                .map_err(KclErrorWithOutputs::no_outputs)?;
929        }
930
931        self.execute_after_edit(
932            ctx,
933            sketch,
934            sketch_block_ref,
935            Default::default(),
936            EditDeleteKind::DeleteNonSketch,
937            &mut new_ast,
938        )
939        .await
940    }
941
942    async fn add_constraint(
943        &mut self,
944        ctx: &ExecutorContext,
945        _version: Version,
946        sketch: ObjectId,
947        constraint: Constraint,
948    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
949        // TODO: Check version.
950
951        // Save the original state as a backup - we'll restore it if anything fails
952        let original_program = self.program.clone();
953        let original_scene_graph = self.scene_graph.clone();
954
955        let mut new_ast = self.program.ast.clone();
956        let sketch_block_ref = match constraint {
957            Constraint::Coincident(coincident) => self
958                .add_coincident(sketch, coincident, &mut new_ast)
959                .await
960                .map_err(KclErrorWithOutputs::no_outputs)?,
961            Constraint::Distance(distance) => self
962                .add_distance(sketch, distance, &mut new_ast)
963                .await
964                .map_err(KclErrorWithOutputs::no_outputs)?,
965            Constraint::EqualRadius(equal_radius) => self
966                .add_equal_radius(sketch, equal_radius, &mut new_ast)
967                .await
968                .map_err(KclErrorWithOutputs::no_outputs)?,
969            Constraint::Fixed(fixed) => self
970                .add_fixed_constraints(sketch, fixed.points, &mut new_ast)
971                .await
972                .map_err(KclErrorWithOutputs::no_outputs)?,
973            Constraint::HorizontalDistance(distance) => self
974                .add_horizontal_distance(sketch, distance, &mut new_ast)
975                .await
976                .map_err(KclErrorWithOutputs::no_outputs)?,
977            Constraint::VerticalDistance(distance) => self
978                .add_vertical_distance(sketch, distance, &mut new_ast)
979                .await
980                .map_err(KclErrorWithOutputs::no_outputs)?,
981            Constraint::Horizontal(horizontal) => self
982                .add_horizontal(sketch, horizontal, &mut new_ast)
983                .await
984                .map_err(KclErrorWithOutputs::no_outputs)?,
985            Constraint::LinesEqualLength(lines_equal_length) => self
986                .add_lines_equal_length(sketch, lines_equal_length, &mut new_ast)
987                .await
988                .map_err(KclErrorWithOutputs::no_outputs)?,
989            Constraint::Midpoint(midpoint) => self
990                .add_midpoint(sketch, midpoint, &mut new_ast)
991                .await
992                .map_err(KclErrorWithOutputs::no_outputs)?,
993            Constraint::Parallel(parallel) => self
994                .add_parallel(sketch, parallel, &mut new_ast)
995                .await
996                .map_err(KclErrorWithOutputs::no_outputs)?,
997            Constraint::Perpendicular(perpendicular) => self
998                .add_perpendicular(sketch, perpendicular, &mut new_ast)
999                .await
1000                .map_err(KclErrorWithOutputs::no_outputs)?,
1001            Constraint::Radius(radius) => self
1002                .add_radius(sketch, radius, &mut new_ast)
1003                .await
1004                .map_err(KclErrorWithOutputs::no_outputs)?,
1005            Constraint::Diameter(diameter) => self
1006                .add_diameter(sketch, diameter, &mut new_ast)
1007                .await
1008                .map_err(KclErrorWithOutputs::no_outputs)?,
1009            Constraint::Vertical(vertical) => self
1010                .add_vertical(sketch, vertical, &mut new_ast)
1011                .await
1012                .map_err(KclErrorWithOutputs::no_outputs)?,
1013            Constraint::Angle(lines_at_angle) => self
1014                .add_angle(sketch, lines_at_angle, &mut new_ast)
1015                .await
1016                .map_err(KclErrorWithOutputs::no_outputs)?,
1017            Constraint::Tangent(tangent) => self
1018                .add_tangent(sketch, tangent, &mut new_ast)
1019                .await
1020                .map_err(KclErrorWithOutputs::no_outputs)?,
1021        };
1022
1023        let result = self
1024            .execute_after_add_constraint(ctx, sketch, sketch_block_ref, &mut new_ast)
1025            .await;
1026
1027        // If execution failed, restore the original state to prevent corruption
1028        if result.is_err() {
1029            self.program = original_program;
1030            self.scene_graph = original_scene_graph;
1031        }
1032
1033        result
1034    }
1035
1036    async fn chain_segment(
1037        &mut self,
1038        ctx: &ExecutorContext,
1039        version: Version,
1040        sketch: ObjectId,
1041        previous_segment_end_point_id: ObjectId,
1042        segment: SegmentCtor,
1043        _label: Option<String>,
1044    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1045        // TODO: Check version.
1046
1047        // First, add the segment (line) to get its start point ID
1048        let SegmentCtor::Line(line_ctor) = segment else {
1049            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1050                "chain_segment currently only supports Line segments, got {}",
1051                segment.human_friendly_kind_with_article(),
1052            ))));
1053        };
1054
1055        // Add the line segment first - this updates self.program and self.scene_graph
1056        let (_first_src_delta, first_scene_delta) = self.add_line(ctx, sketch, line_ctor).await?;
1057
1058        // Find the new line's start point ID from the updated scene graph
1059        // add_line updates self.scene_graph, so we can use that
1060        let new_line_id = first_scene_delta
1061            .new_objects
1062            .iter()
1063            .find(|&obj_id| {
1064                let obj = self.scene_graph.objects.get(obj_id.0);
1065                if let Some(obj) = obj {
1066                    matches!(
1067                        &obj.kind,
1068                        ObjectKind::Segment {
1069                            segment: Segment::Line(_)
1070                        }
1071                    )
1072                } else {
1073                    false
1074                }
1075            })
1076            .ok_or_else(|| {
1077                KclErrorWithOutputs::no_outputs(KclError::refactor(
1078                    "Failed to find new line segment in scene graph".to_string(),
1079                ))
1080            })?;
1081
1082        let new_line_obj = self.scene_graph.objects.get(new_line_id.0).ok_or_else(|| {
1083            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1084                "New line object not found: {new_line_id:?}"
1085            )))
1086        })?;
1087
1088        let ObjectKind::Segment {
1089            segment: new_line_segment,
1090        } = &new_line_obj.kind
1091        else {
1092            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1093                "Object is not a segment: {new_line_obj:?}"
1094            ))));
1095        };
1096
1097        let Segment::Line(new_line) = new_line_segment else {
1098            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1099                "Segment is not a line: {new_line_segment:?}"
1100            ))));
1101        };
1102
1103        let new_line_start_point_id = new_line.start;
1104
1105        // Now add the coincident constraint between the previous end point and the new line's start point.
1106        let coincident = Coincident {
1107            segments: vec![previous_segment_end_point_id.into(), new_line_start_point_id.into()],
1108        };
1109
1110        let (final_src_delta, final_scene_delta) = self
1111            .add_constraint(ctx, version, sketch, Constraint::Coincident(coincident))
1112            .await?;
1113
1114        // Combine new objects from the line addition and the constraint addition.
1115        // Both add_line and add_constraint now populate new_objects correctly.
1116        let mut combined_new_objects = first_scene_delta.new_objects.clone();
1117        combined_new_objects.extend(final_scene_delta.new_objects);
1118
1119        let scene_graph_delta = SceneGraphDelta {
1120            new_graph: self.scene_graph.clone(),
1121            invalidates_ids: false,
1122            new_objects: combined_new_objects,
1123            exec_outcome: final_scene_delta.exec_outcome,
1124        };
1125
1126        Ok((final_src_delta, scene_graph_delta))
1127    }
1128
1129    async fn edit_constraint(
1130        &mut self,
1131        ctx: &ExecutorContext,
1132        _version: Version,
1133        sketch: ObjectId,
1134        constraint_id: ObjectId,
1135        value_expression: String,
1136    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1137        // TODO: Check version.
1138        let sketch_block_ref =
1139            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1140
1141        let object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
1142            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Object not found: {constraint_id:?}")))
1143        })?;
1144        if !matches!(&object.kind, ObjectKind::Constraint { .. }) {
1145            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1146                "Object is not a constraint: {constraint_id:?}"
1147            ))));
1148        }
1149
1150        let mut new_ast = self.program.ast.clone();
1151
1152        // Parse the expression string into an AST node.
1153        let (parsed, errors) = Program::parse(&value_expression)
1154            .map_err(|e| KclErrorWithOutputs::no_outputs(KclError::refactor(e.to_string())))?;
1155        if !errors.is_empty() {
1156            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1157                "Error parsing value expression: {errors:?}"
1158            ))));
1159        }
1160        let mut parsed = parsed.ok_or_else(|| {
1161            KclErrorWithOutputs::no_outputs(KclError::refactor("No AST produced from value expression".to_string()))
1162        })?;
1163        if parsed.ast.body.is_empty() {
1164            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1165                "Empty value expression".to_string(),
1166            )));
1167        }
1168        let first = parsed.ast.body.remove(0);
1169        let ast::BodyItem::ExpressionStatement(expr_stmt) = first else {
1170            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1171                "Value expression must be a simple expression".to_string(),
1172            )));
1173        };
1174
1175        let new_value: ast::BinaryPart = expr_stmt
1176            .inner
1177            .expression
1178            .try_into()
1179            .map_err(|e: String| KclErrorWithOutputs::no_outputs(KclError::refactor(e)))?;
1180
1181        self.mutate_ast(
1182            &mut new_ast,
1183            constraint_id,
1184            AstMutateCommand::EditConstraintValue { value: new_value },
1185        )
1186        .map_err(KclErrorWithOutputs::no_outputs)?;
1187
1188        self.execute_after_edit(
1189            ctx,
1190            sketch,
1191            sketch_block_ref,
1192            Default::default(),
1193            EditDeleteKind::Edit,
1194            &mut new_ast,
1195        )
1196        .await
1197    }
1198
1199    /// Splitting a segment means creating a new segment, editing the old one, and then
1200    /// migrating a bunch of the constraints from the original segment to the new one
1201    /// (i.e. deleting them and re-adding them on the other segment).
1202    ///
1203    /// To keep this efficient we require as few executions as possible: we create the
1204    /// new segment first (to get its id), then do all edits and new constraints, and
1205    /// do all deletes at the end (since deletes invalidate ids).
1206    async fn batch_split_segment_operations(
1207        &mut self,
1208        ctx: &ExecutorContext,
1209        _version: Version,
1210        sketch: ObjectId,
1211        edit_segments: Vec<ExistingSegmentCtor>,
1212        add_constraints: Vec<Constraint>,
1213        delete_constraint_ids: Vec<ObjectId>,
1214        _new_segment_info: sketch::NewSegmentInfo,
1215    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1216        // TODO: Check version.
1217        let sketch_block_ref =
1218            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1219
1220        let mut new_ast = self.program.ast.clone();
1221        let mut segment_ids_edited = AhashIndexSet::with_capacity_and_hasher(edit_segments.len(), Default::default());
1222
1223        // Step 1: Edit segments
1224        for segment in edit_segments {
1225            segment_ids_edited.insert(segment.id);
1226            match segment.ctor {
1227                SegmentCtor::Point(ctor) => self
1228                    .edit_point(&mut new_ast, sketch, segment.id, ctor)
1229                    .map_err(KclErrorWithOutputs::no_outputs)?,
1230                SegmentCtor::Line(ctor) => self
1231                    .edit_line(&mut new_ast, sketch, segment.id, ctor)
1232                    .map_err(KclErrorWithOutputs::no_outputs)?,
1233                SegmentCtor::Arc(ctor) => self
1234                    .edit_arc(&mut new_ast, sketch, segment.id, ctor)
1235                    .map_err(KclErrorWithOutputs::no_outputs)?,
1236                SegmentCtor::Circle(ctor) => self
1237                    .edit_circle(&mut new_ast, sketch, segment.id, ctor)
1238                    .map_err(KclErrorWithOutputs::no_outputs)?,
1239            }
1240        }
1241
1242        // Step 2: Add all constraints
1243        for constraint in add_constraints {
1244            match constraint {
1245                Constraint::Coincident(coincident) => {
1246                    self.add_coincident(sketch, coincident, &mut new_ast)
1247                        .await
1248                        .map_err(KclErrorWithOutputs::no_outputs)?;
1249                }
1250                Constraint::Distance(distance) => {
1251                    self.add_distance(sketch, distance, &mut new_ast)
1252                        .await
1253                        .map_err(KclErrorWithOutputs::no_outputs)?;
1254                }
1255                Constraint::EqualRadius(equal_radius) => {
1256                    self.add_equal_radius(sketch, equal_radius, &mut new_ast)
1257                        .await
1258                        .map_err(KclErrorWithOutputs::no_outputs)?;
1259                }
1260                Constraint::Fixed(fixed) => {
1261                    self.add_fixed_constraints(sketch, fixed.points, &mut new_ast)
1262                        .await
1263                        .map_err(KclErrorWithOutputs::no_outputs)?;
1264                }
1265                Constraint::HorizontalDistance(distance) => {
1266                    self.add_horizontal_distance(sketch, distance, &mut new_ast)
1267                        .await
1268                        .map_err(KclErrorWithOutputs::no_outputs)?;
1269                }
1270                Constraint::VerticalDistance(distance) => {
1271                    self.add_vertical_distance(sketch, distance, &mut new_ast)
1272                        .await
1273                        .map_err(KclErrorWithOutputs::no_outputs)?;
1274                }
1275                Constraint::Horizontal(horizontal) => {
1276                    self.add_horizontal(sketch, horizontal, &mut new_ast)
1277                        .await
1278                        .map_err(KclErrorWithOutputs::no_outputs)?;
1279                }
1280                Constraint::LinesEqualLength(lines_equal_length) => {
1281                    self.add_lines_equal_length(sketch, lines_equal_length, &mut new_ast)
1282                        .await
1283                        .map_err(KclErrorWithOutputs::no_outputs)?;
1284                }
1285                Constraint::Midpoint(midpoint) => {
1286                    self.add_midpoint(sketch, midpoint, &mut new_ast)
1287                        .await
1288                        .map_err(KclErrorWithOutputs::no_outputs)?;
1289                }
1290                Constraint::Parallel(parallel) => {
1291                    self.add_parallel(sketch, parallel, &mut new_ast)
1292                        .await
1293                        .map_err(KclErrorWithOutputs::no_outputs)?;
1294                }
1295                Constraint::Perpendicular(perpendicular) => {
1296                    self.add_perpendicular(sketch, perpendicular, &mut new_ast)
1297                        .await
1298                        .map_err(KclErrorWithOutputs::no_outputs)?;
1299                }
1300                Constraint::Vertical(vertical) => {
1301                    self.add_vertical(sketch, vertical, &mut new_ast)
1302                        .await
1303                        .map_err(KclErrorWithOutputs::no_outputs)?;
1304                }
1305                Constraint::Diameter(diameter) => {
1306                    self.add_diameter(sketch, diameter, &mut new_ast)
1307                        .await
1308                        .map_err(KclErrorWithOutputs::no_outputs)?;
1309                }
1310                Constraint::Radius(radius) => {
1311                    self.add_radius(sketch, radius, &mut new_ast)
1312                        .await
1313                        .map_err(KclErrorWithOutputs::no_outputs)?;
1314                }
1315                Constraint::Angle(angle) => {
1316                    self.add_angle(sketch, angle, &mut new_ast)
1317                        .await
1318                        .map_err(KclErrorWithOutputs::no_outputs)?;
1319                }
1320                Constraint::Tangent(tangent) => {
1321                    self.add_tangent(sketch, tangent, &mut new_ast)
1322                        .await
1323                        .map_err(KclErrorWithOutputs::no_outputs)?;
1324                }
1325            }
1326        }
1327
1328        // Step 3: Delete constraints (must be last since deletes can invalidate IDs)
1329        let constraint_ids_set = delete_constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
1330
1331        let has_constraint_deletions = !constraint_ids_set.is_empty();
1332        for constraint_id in constraint_ids_set {
1333            self.delete_constraint(&mut new_ast, sketch, constraint_id)
1334                .map_err(KclErrorWithOutputs::no_outputs)?;
1335        }
1336
1337        // Step 4: Execute once at the end
1338        // Always use Edit (not DeleteNonSketch) because we're editing the sketch block, not deleting it
1339        // But we'll manually set invalidates_ids: true if we deleted constraints
1340        let (source_delta, mut scene_graph_delta) = self
1341            .execute_after_edit(
1342                ctx,
1343                sketch,
1344                sketch_block_ref,
1345                segment_ids_edited,
1346                EditDeleteKind::Edit,
1347                &mut new_ast,
1348            )
1349            .await?;
1350
1351        // If we deleted constraints, set invalidates_ids: true
1352        // This is because constraint deletion invalidates IDs, even though we're not deleting the sketch block
1353        if has_constraint_deletions {
1354            scene_graph_delta.invalidates_ids = true;
1355        }
1356
1357        Ok((source_delta, scene_graph_delta))
1358    }
1359
1360    async fn batch_tail_cut_operations(
1361        &mut self,
1362        ctx: &ExecutorContext,
1363        _version: Version,
1364        sketch: ObjectId,
1365        edit_segments: Vec<ExistingSegmentCtor>,
1366        add_constraints: Vec<Constraint>,
1367        delete_constraint_ids: Vec<ObjectId>,
1368    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1369        let sketch_block_ref =
1370            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1371
1372        let mut new_ast = self.program.ast.clone();
1373        let mut segment_ids_edited = AhashIndexSet::with_capacity_and_hasher(edit_segments.len(), Default::default());
1374
1375        // Step 1: Edit segments (usually a single segment for tail cut)
1376        for segment in edit_segments {
1377            segment_ids_edited.insert(segment.id);
1378            match segment.ctor {
1379                SegmentCtor::Point(ctor) => self
1380                    .edit_point(&mut new_ast, sketch, segment.id, ctor)
1381                    .map_err(KclErrorWithOutputs::no_outputs)?,
1382                SegmentCtor::Line(ctor) => self
1383                    .edit_line(&mut new_ast, sketch, segment.id, ctor)
1384                    .map_err(KclErrorWithOutputs::no_outputs)?,
1385                SegmentCtor::Arc(ctor) => self
1386                    .edit_arc(&mut new_ast, sketch, segment.id, ctor)
1387                    .map_err(KclErrorWithOutputs::no_outputs)?,
1388                SegmentCtor::Circle(ctor) => self
1389                    .edit_circle(&mut new_ast, sketch, segment.id, ctor)
1390                    .map_err(KclErrorWithOutputs::no_outputs)?,
1391            }
1392        }
1393
1394        // Step 2: Add coincident constraints
1395        for constraint in add_constraints {
1396            match constraint {
1397                Constraint::Coincident(coincident) => {
1398                    self.add_coincident(sketch, coincident, &mut new_ast)
1399                        .await
1400                        .map_err(KclErrorWithOutputs::no_outputs)?;
1401                }
1402                other => {
1403                    return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1404                        "unsupported constraint in tail cut batch: {other:?}"
1405                    ))));
1406                }
1407            }
1408        }
1409
1410        // Step 3: Delete constraints (if any)
1411        let constraint_ids_set = delete_constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
1412
1413        let has_constraint_deletions = !constraint_ids_set.is_empty();
1414        for constraint_id in constraint_ids_set {
1415            self.delete_constraint(&mut new_ast, sketch, constraint_id)
1416                .map_err(KclErrorWithOutputs::no_outputs)?;
1417        }
1418
1419        // Step 4: Single execute_after_edit
1420        // Always use Edit (not DeleteNonSketch) because we're editing the sketch block, not deleting it
1421        // But we'll manually set invalidates_ids: true if we deleted constraints
1422        let (source_delta, mut scene_graph_delta) = self
1423            .execute_after_edit(
1424                ctx,
1425                sketch,
1426                sketch_block_ref,
1427                segment_ids_edited,
1428                EditDeleteKind::Edit,
1429                &mut new_ast,
1430            )
1431            .await?;
1432
1433        // If we deleted constraints, set invalidates_ids: true
1434        // This is because constraint deletion invalidates IDs, even though we're not deleting the sketch block
1435        if has_constraint_deletions {
1436            scene_graph_delta.invalidates_ids = true;
1437        }
1438
1439        Ok((source_delta, scene_graph_delta))
1440    }
1441}
1442
1443impl FrontendState {
1444    pub async fn hack_set_program(&mut self, ctx: &ExecutorContext, program: Program) -> ExecResult<SetProgramOutcome> {
1445        self.program = program.clone();
1446
1447        // Execute so that the objects are updated and available for the next
1448        // API call.
1449        // This always uses engine execution (not mock) so that things are cached.
1450        // Engine execution now runs freedom analysis automatically.
1451        // Keep existing checkpoints alive here. History may still reference
1452        // older committed sketch states across a direct-edit boundary, and a
1453        // checkpoint restore is a full state replacement anyway. We append a
1454        // fresh baseline checkpoint after the full execution below.
1455        // Clear the freedom cache since IDs might have changed after direct editing
1456        // and we're about to run freedom analysis which will repopulate it.
1457        self.point_freedom_cache.clear();
1458        match ctx.run_with_caching(program).await {
1459            Ok(outcome) => {
1460                let outcome = self.update_state_after_exec(outcome, true);
1461                let checkpoint_id = self
1462                    .create_sketch_checkpoint(outcome.clone())
1463                    .await
1464                    .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
1465                Ok(SetProgramOutcome::Success {
1466                    scene_graph: Box::new(self.scene_graph.clone()),
1467                    exec_outcome: Box::new(outcome),
1468                    checkpoint_id: Some(checkpoint_id),
1469                })
1470            }
1471            Err(mut err) => {
1472                // Don't return an error just because execution failed. Instead,
1473                // update state as much as possible.
1474                let outcome = self.exec_outcome_from_exec_error(err.clone())?;
1475                self.update_state_after_exec(outcome, true);
1476                err.scene_graph = Some(self.scene_graph.clone());
1477                Ok(SetProgramOutcome::ExecFailure { error: Box::new(err) })
1478            }
1479        }
1480    }
1481
1482    /// Decorate engine execution such that our state is updated and the scene
1483    /// graph is added to the return.
1484    pub async fn engine_execute(
1485        &mut self,
1486        ctx: &ExecutorContext,
1487        program: Program,
1488    ) -> Result<SceneGraphDelta, KclErrorWithOutputs> {
1489        self.program = program.clone();
1490
1491        // Engine execution now runs freedom analysis automatically. Clear the
1492        // freedom cache since IDs might have changed after direct editing, and
1493        // we're about to run freedom analysis which will repopulate it.
1494        self.point_freedom_cache.clear();
1495        match ctx.run_with_caching(program).await {
1496            Ok(outcome) => {
1497                let outcome = self.update_state_after_exec(outcome, true);
1498                Ok(SceneGraphDelta {
1499                    new_graph: self.scene_graph.clone(),
1500                    exec_outcome: outcome,
1501                    // We don't know what the new objects are.
1502                    new_objects: Default::default(),
1503                    // We don't know if IDs were invalidated.
1504                    invalidates_ids: Default::default(),
1505                })
1506            }
1507            Err(mut err) => {
1508                // Update state as much as possible, even when there's an error.
1509                let outcome = self.exec_outcome_from_exec_error(err.clone())?;
1510                self.update_state_after_exec(outcome, true);
1511                err.scene_graph = Some(self.scene_graph.clone());
1512                Err(err)
1513            }
1514        }
1515    }
1516
1517    fn exec_outcome_from_exec_error(&self, err: KclErrorWithOutputs) -> Result<ExecOutcome, KclErrorWithOutputs> {
1518        if matches!(err.error, KclError::EngineHangup { .. }) {
1519            // It's not ideal to special-case this, but this error is very
1520            // common during development, and it causes confusing downstream
1521            // errors that have nothing to do with the actual problem.
1522            return Err(err);
1523        }
1524
1525        let KclErrorWithOutputs {
1526            error,
1527            mut non_fatal,
1528            variables,
1529            #[cfg(feature = "artifact-graph")]
1530            operations,
1531            #[cfg(feature = "artifact-graph")]
1532            artifact_graph,
1533            #[cfg(feature = "artifact-graph")]
1534            scene_objects,
1535            #[cfg(feature = "artifact-graph")]
1536            source_range_to_object,
1537            #[cfg(feature = "artifact-graph")]
1538            var_solutions,
1539            filenames,
1540            default_planes,
1541            ..
1542        } = err;
1543
1544        if let Some(source_range) = error.source_ranges().first() {
1545            non_fatal.push(CompilationIssue::fatal(*source_range, error.get_message()));
1546        } else {
1547            non_fatal.push(CompilationIssue::fatal(SourceRange::synthetic(), error.get_message()));
1548        }
1549
1550        Ok(ExecOutcome {
1551            variables,
1552            filenames,
1553            #[cfg(feature = "artifact-graph")]
1554            operations,
1555            #[cfg(feature = "artifact-graph")]
1556            artifact_graph,
1557            #[cfg(feature = "artifact-graph")]
1558            scene_objects,
1559            #[cfg(feature = "artifact-graph")]
1560            source_range_to_object,
1561            #[cfg(feature = "artifact-graph")]
1562            var_solutions,
1563            issues: non_fatal,
1564            default_planes,
1565        })
1566    }
1567
1568    async fn add_point(
1569        &mut self,
1570        ctx: &ExecutorContext,
1571        sketch: ObjectId,
1572        ctor: PointCtor,
1573    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1574        // Create updated KCL source from args.
1575        let at_ast = to_ast_point2d(&ctor.position)
1576            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1577        let point_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
1578            callee: ast::Node::no_src(ast_sketch2_name(POINT_FN)),
1579            unlabeled: None,
1580            arguments: vec![ast::LabeledArg {
1581                label: Some(ast::Identifier::new(POINT_AT_PARAM)),
1582                arg: at_ast,
1583            }],
1584            digest: None,
1585            non_code_meta: Default::default(),
1586        })));
1587
1588        // Look up existing sketch.
1589        let sketch_id = sketch;
1590        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
1591            #[cfg(target_arch = "wasm32")]
1592            web_sys::console::error_1(
1593                &format!(
1594                    "Sketch not found; sketch_id={sketch_id:?}, self.scene_graph.objects={:#?}",
1595                    &self.scene_graph.objects
1596                )
1597                .into(),
1598            );
1599            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
1600        })?;
1601        let ObjectKind::Sketch(_) = &sketch_object.kind else {
1602            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1603                "Object is not a sketch, it is {}",
1604                sketch_object.kind.human_friendly_kind_with_article(),
1605            ))));
1606        };
1607        // Add the point to the AST of the sketch block.
1608        let mut new_ast = self.program.ast.clone();
1609        let (sketch_block_ref, _) = self
1610            .mutate_ast(
1611                &mut new_ast,
1612                sketch_id,
1613                AstMutateCommand::AddSketchBlockExprStmt { expr: point_ast },
1614            )
1615            .map_err(KclErrorWithOutputs::no_outputs)?;
1616        // Convert to string source to create real source ranges.
1617        let new_source = source_from_ast(&new_ast);
1618        // Parse the new KCL source.
1619        let (new_program, errors) = Program::parse(&new_source)
1620            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1621        if !errors.is_empty() {
1622            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1623                "Error parsing KCL source after adding point: {errors:?}"
1624            ))));
1625        }
1626        let Some(new_program) = new_program else {
1627            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1628                "No AST produced after adding point".to_string(),
1629            )));
1630        };
1631
1632        let point_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
1633            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1634                "Source range of point not found in sketch block: {sketch_block_ref:?}; {err:?}"
1635            )))
1636        })?;
1637        #[cfg(not(feature = "artifact-graph"))]
1638        let _ = point_node_ref;
1639
1640        // Make sure to only set this if there are no errors.
1641        self.program = new_program.clone();
1642
1643        // Truncate after the sketch block for mock execution.
1644        let mut truncated_program = new_program;
1645        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
1646            .map_err(KclErrorWithOutputs::no_outputs)?;
1647
1648        // Execute.
1649        let outcome = ctx
1650            .run_mock(
1651                &truncated_program,
1652                &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
1653            )
1654            .await?;
1655
1656        #[cfg(not(feature = "artifact-graph"))]
1657        let new_object_ids = Vec::new();
1658        #[cfg(feature = "artifact-graph")]
1659        let new_object_ids = {
1660            let make_err =
1661                |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
1662            let segment_id = outcome
1663                .source_range_to_object
1664                .get(&point_node_ref.range)
1665                .copied()
1666                .ok_or_else(|| make_err(format!("Source range of point not found: {point_node_ref:?}")))?;
1667            let segment_object = outcome
1668                .scene_objects
1669                .get(segment_id.0)
1670                .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
1671            let ObjectKind::Segment { segment } = &segment_object.kind else {
1672                return Err(make_err(format!(
1673                    "Object is not a segment, it is {}",
1674                    segment_object.kind.human_friendly_kind_with_article()
1675                )));
1676            };
1677            let Segment::Point(_) = segment else {
1678                return Err(make_err(format!(
1679                    "Segment is not a point, it is {}",
1680                    segment.human_friendly_kind_with_article()
1681                )));
1682            };
1683            vec![segment_id]
1684        };
1685        let src_delta = SourceDelta { text: new_source };
1686        // Uses .no_freedom_analysis() so freedom_analysis: false
1687        let outcome = self.update_state_after_exec(outcome, false);
1688        let scene_graph_delta = SceneGraphDelta {
1689            new_graph: self.scene_graph.clone(),
1690            invalidates_ids: false,
1691            new_objects: new_object_ids,
1692            exec_outcome: outcome,
1693        };
1694        Ok((src_delta, scene_graph_delta))
1695    }
1696
1697    async fn add_line(
1698        &mut self,
1699        ctx: &ExecutorContext,
1700        sketch: ObjectId,
1701        ctor: LineCtor,
1702    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1703        // Create updated KCL source from args.
1704        let start_ast = to_ast_point2d(&ctor.start)
1705            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1706        let end_ast = to_ast_point2d(&ctor.end)
1707            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1708        let mut arguments = vec![
1709            ast::LabeledArg {
1710                label: Some(ast::Identifier::new(LINE_START_PARAM)),
1711                arg: start_ast,
1712            },
1713            ast::LabeledArg {
1714                label: Some(ast::Identifier::new(LINE_END_PARAM)),
1715                arg: end_ast,
1716            },
1717        ];
1718        // Add construction kwarg if construction is Some(true)
1719        if ctor.construction == Some(true) {
1720            arguments.push(ast::LabeledArg {
1721                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
1722                arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
1723                    value: ast::LiteralValue::Bool(true),
1724                    raw: "true".to_string(),
1725                    digest: None,
1726                }))),
1727            });
1728        }
1729        let line_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
1730            callee: ast::Node::no_src(ast_sketch2_name(LINE_FN)),
1731            unlabeled: None,
1732            arguments,
1733            digest: None,
1734            non_code_meta: Default::default(),
1735        })));
1736
1737        // Look up existing sketch.
1738        let sketch_id = sketch;
1739        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
1740            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
1741        })?;
1742        let ObjectKind::Sketch(_) = &sketch_object.kind else {
1743            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1744                "Object is not a sketch, it is {}",
1745                sketch_object.kind.human_friendly_kind_with_article(),
1746            ))));
1747        };
1748        // Add the line to the AST of the sketch block.
1749        let mut new_ast = self.program.ast.clone();
1750        let (sketch_block_ref, _) = self
1751            .mutate_ast(
1752                &mut new_ast,
1753                sketch_id,
1754                AstMutateCommand::AddSketchBlockExprStmt { expr: line_ast },
1755            )
1756            .map_err(KclErrorWithOutputs::no_outputs)?;
1757        // Convert to string source to create real source ranges.
1758        let new_source = source_from_ast(&new_ast);
1759        // Parse the new KCL source.
1760        let (new_program, errors) = Program::parse(&new_source)
1761            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1762        if !errors.is_empty() {
1763            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1764                "Error parsing KCL source after adding line: {errors:?}"
1765            ))));
1766        }
1767        let Some(new_program) = new_program else {
1768            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1769                "No AST produced after adding line".to_string(),
1770            )));
1771        };
1772
1773        let line_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
1774            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1775                "Source range of line not found in sketch block: {sketch_block_ref:?}; {err:?}"
1776            )))
1777        })?;
1778        #[cfg(not(feature = "artifact-graph"))]
1779        let _ = line_node_ref;
1780
1781        // Make sure to only set this if there are no errors.
1782        self.program = new_program.clone();
1783
1784        // Truncate after the sketch block for mock execution.
1785        let mut truncated_program = new_program;
1786        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
1787            .map_err(KclErrorWithOutputs::no_outputs)?;
1788
1789        // Execute.
1790        let outcome = ctx
1791            .run_mock(
1792                &truncated_program,
1793                &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
1794            )
1795            .await?;
1796
1797        #[cfg(not(feature = "artifact-graph"))]
1798        let new_object_ids = Vec::new();
1799        #[cfg(feature = "artifact-graph")]
1800        let new_object_ids = {
1801            let make_err =
1802                |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
1803            let segment_id = outcome
1804                .source_range_to_object
1805                .get(&line_node_ref.range)
1806                .copied()
1807                .ok_or_else(|| make_err(format!("Source range of line not found: {line_node_ref:?}")))?;
1808            let segment_object = outcome
1809                .scene_object_by_id(segment_id)
1810                .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
1811            let ObjectKind::Segment { segment } = &segment_object.kind else {
1812                return Err(make_err(format!(
1813                    "Object is not a segment, it is {}",
1814                    segment_object.kind.human_friendly_kind_with_article()
1815                )));
1816            };
1817            let Segment::Line(line) = segment else {
1818                return Err(make_err(format!(
1819                    "Segment is not a line, it is {}",
1820                    segment.human_friendly_kind_with_article()
1821                )));
1822            };
1823            vec![line.start, line.end, segment_id]
1824        };
1825        let src_delta = SourceDelta { text: new_source };
1826        // Uses .no_freedom_analysis() so freedom_analysis: false
1827        let outcome = self.update_state_after_exec(outcome, false);
1828        let scene_graph_delta = SceneGraphDelta {
1829            new_graph: self.scene_graph.clone(),
1830            invalidates_ids: false,
1831            new_objects: new_object_ids,
1832            exec_outcome: outcome,
1833        };
1834        Ok((src_delta, scene_graph_delta))
1835    }
1836
1837    async fn add_arc(
1838        &mut self,
1839        ctx: &ExecutorContext,
1840        sketch: ObjectId,
1841        ctor: ArcCtor,
1842    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1843        // Create updated KCL source from args.
1844        let start_ast = to_ast_point2d(&ctor.start)
1845            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1846        let end_ast = to_ast_point2d(&ctor.end)
1847            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1848        let center_ast = to_ast_point2d(&ctor.center)
1849            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1850        let mut arguments = vec![
1851            ast::LabeledArg {
1852                label: Some(ast::Identifier::new(ARC_START_PARAM)),
1853                arg: start_ast,
1854            },
1855            ast::LabeledArg {
1856                label: Some(ast::Identifier::new(ARC_END_PARAM)),
1857                arg: end_ast,
1858            },
1859            ast::LabeledArg {
1860                label: Some(ast::Identifier::new(ARC_CENTER_PARAM)),
1861                arg: center_ast,
1862            },
1863        ];
1864        // Add construction kwarg if construction is Some(true)
1865        if ctor.construction == Some(true) {
1866            arguments.push(ast::LabeledArg {
1867                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
1868                arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
1869                    value: ast::LiteralValue::Bool(true),
1870                    raw: "true".to_string(),
1871                    digest: None,
1872                }))),
1873            });
1874        }
1875        let arc_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
1876            callee: ast::Node::no_src(ast_sketch2_name(ARC_FN)),
1877            unlabeled: None,
1878            arguments,
1879            digest: None,
1880            non_code_meta: Default::default(),
1881        })));
1882
1883        // Look up existing sketch.
1884        let sketch_id = sketch;
1885        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
1886            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
1887        })?;
1888        let ObjectKind::Sketch(_) = &sketch_object.kind else {
1889            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1890                "Object is not a sketch, it is {}",
1891                sketch_object.kind.human_friendly_kind_with_article(),
1892            ))));
1893        };
1894        // Add the arc to the AST of the sketch block.
1895        let mut new_ast = self.program.ast.clone();
1896        let (sketch_block_ref, _) = self
1897            .mutate_ast(
1898                &mut new_ast,
1899                sketch_id,
1900                AstMutateCommand::AddSketchBlockExprStmt { expr: arc_ast },
1901            )
1902            .map_err(KclErrorWithOutputs::no_outputs)?;
1903        // Convert to string source to create real source ranges.
1904        let new_source = source_from_ast(&new_ast);
1905        // Parse the new KCL source.
1906        let (new_program, errors) = Program::parse(&new_source)
1907            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1908        if !errors.is_empty() {
1909            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1910                "Error parsing KCL source after adding arc: {errors:?}"
1911            ))));
1912        }
1913        let Some(new_program) = new_program else {
1914            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1915                "No AST produced after adding arc".to_string(),
1916            )));
1917        };
1918
1919        let arc_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
1920            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1921                "Source range of arc not found in sketch block: {sketch_block_ref:?}; {err:?}"
1922            )))
1923        })?;
1924        #[cfg(not(feature = "artifact-graph"))]
1925        let _ = arc_node_ref;
1926
1927        // Make sure to only set this if there are no errors.
1928        self.program = new_program.clone();
1929
1930        // Truncate after the sketch block for mock execution.
1931        let mut truncated_program = new_program;
1932        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
1933            .map_err(KclErrorWithOutputs::no_outputs)?;
1934
1935        // Execute.
1936        let outcome = ctx
1937            .run_mock(
1938                &truncated_program,
1939                &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
1940            )
1941            .await?;
1942
1943        #[cfg(not(feature = "artifact-graph"))]
1944        let new_object_ids = Vec::new();
1945        #[cfg(feature = "artifact-graph")]
1946        let new_object_ids = {
1947            let make_err =
1948                |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
1949            let segment_id = outcome
1950                .source_range_to_object
1951                .get(&arc_node_ref.range)
1952                .copied()
1953                .ok_or_else(|| make_err(format!("Source range of arc not found: {arc_node_ref:?}")))?;
1954            let segment_object = outcome
1955                .scene_objects
1956                .get(segment_id.0)
1957                .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
1958            let ObjectKind::Segment { segment } = &segment_object.kind else {
1959                return Err(make_err(format!(
1960                    "Object is not a segment, it is {}",
1961                    segment_object.kind.human_friendly_kind_with_article()
1962                )));
1963            };
1964            let Segment::Arc(arc) = segment else {
1965                return Err(make_err(format!(
1966                    "Segment is not an arc, it is {}",
1967                    segment.human_friendly_kind_with_article()
1968                )));
1969            };
1970            vec![arc.start, arc.end, arc.center, segment_id]
1971        };
1972        let src_delta = SourceDelta { text: new_source };
1973        // Uses .no_freedom_analysis() so freedom_analysis: false
1974        let outcome = self.update_state_after_exec(outcome, false);
1975        let scene_graph_delta = SceneGraphDelta {
1976            new_graph: self.scene_graph.clone(),
1977            invalidates_ids: false,
1978            new_objects: new_object_ids,
1979            exec_outcome: outcome,
1980        };
1981        Ok((src_delta, scene_graph_delta))
1982    }
1983
1984    async fn add_circle(
1985        &mut self,
1986        ctx: &ExecutorContext,
1987        sketch: ObjectId,
1988        ctor: CircleCtor,
1989    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1990        // Create updated KCL source from args.
1991        let start_ast = to_ast_point2d(&ctor.start)
1992            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1993        let center_ast = to_ast_point2d(&ctor.center)
1994            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1995        let mut arguments = vec![
1996            ast::LabeledArg {
1997                label: Some(ast::Identifier::new(CIRCLE_START_PARAM)),
1998                arg: start_ast,
1999            },
2000            ast::LabeledArg {
2001                label: Some(ast::Identifier::new(CIRCLE_CENTER_PARAM)),
2002                arg: center_ast,
2003            },
2004        ];
2005        // Add construction kwarg if construction is Some(true)
2006        if ctor.construction == Some(true) {
2007            arguments.push(ast::LabeledArg {
2008                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2009                arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
2010                    value: ast::LiteralValue::Bool(true),
2011                    raw: "true".to_string(),
2012                    digest: None,
2013                }))),
2014            });
2015        }
2016        let circle_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
2017            callee: ast::Node::no_src(ast_sketch2_name(CIRCLE_FN)),
2018            unlabeled: None,
2019            arguments,
2020            digest: None,
2021            non_code_meta: Default::default(),
2022        })));
2023
2024        // Look up existing sketch.
2025        let sketch_id = sketch;
2026        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2027            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2028        })?;
2029        let ObjectKind::Sketch(_) = &sketch_object.kind else {
2030            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2031                "Object is not a sketch, it is {}",
2032                sketch_object.kind.human_friendly_kind_with_article(),
2033            ))));
2034        };
2035        // Add the circle to the AST of the sketch block.
2036        let mut new_ast = self.program.ast.clone();
2037        let (sketch_block_ref, _) = self
2038            .mutate_ast(
2039                &mut new_ast,
2040                sketch_id,
2041                AstMutateCommand::AddSketchBlockVarDecl {
2042                    prefix: CIRCLE_VARIABLE.to_owned(),
2043                    expr: circle_ast,
2044                },
2045            )
2046            .map_err(KclErrorWithOutputs::no_outputs)?;
2047        // Convert to string source to create real source ranges.
2048        let new_source = source_from_ast(&new_ast);
2049        // Parse the new KCL source.
2050        let (new_program, errors) = Program::parse(&new_source)
2051            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2052        if !errors.is_empty() {
2053            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2054                "Error parsing KCL source after adding circle: {errors:?}"
2055            ))));
2056        }
2057        let Some(new_program) = new_program else {
2058            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
2059                "No AST produced after adding circle".to_string(),
2060            )));
2061        };
2062
2063        let circle_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2064            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2065                "Source range of circle not found in sketch block: {sketch_block_ref:?}; {err:?}"
2066            )))
2067        })?;
2068        #[cfg(not(feature = "artifact-graph"))]
2069        let _ = circle_node_ref;
2070
2071        // Make sure to only set this if there are no errors.
2072        self.program = new_program.clone();
2073
2074        // Truncate after the sketch block for mock execution.
2075        let mut truncated_program = new_program;
2076        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2077            .map_err(KclErrorWithOutputs::no_outputs)?;
2078
2079        // Execute.
2080        let outcome = ctx
2081            .run_mock(
2082                &truncated_program,
2083                &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2084            )
2085            .await?;
2086
2087        #[cfg(not(feature = "artifact-graph"))]
2088        let new_object_ids = Vec::new();
2089        #[cfg(feature = "artifact-graph")]
2090        let new_object_ids = {
2091            let make_err =
2092                |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2093            let segment_id = outcome
2094                .source_range_to_object
2095                .get(&circle_node_ref.range)
2096                .copied()
2097                .ok_or_else(|| make_err(format!("Source range of circle not found: {circle_node_ref:?}")))?;
2098            let segment_object = outcome
2099                .scene_objects
2100                .get(segment_id.0)
2101                .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2102            let ObjectKind::Segment { segment } = &segment_object.kind else {
2103                return Err(make_err(format!(
2104                    "Object is not a segment, it is {}",
2105                    segment_object.kind.human_friendly_kind_with_article()
2106                )));
2107            };
2108            let Segment::Circle(circle) = segment else {
2109                return Err(make_err(format!(
2110                    "Segment is not a circle, it is {}",
2111                    segment.human_friendly_kind_with_article()
2112                )));
2113            };
2114            vec![circle.start, circle.center, segment_id]
2115        };
2116        let src_delta = SourceDelta { text: new_source };
2117        // Uses .no_freedom_analysis() so freedom_analysis: false
2118        let outcome = self.update_state_after_exec(outcome, false);
2119        let scene_graph_delta = SceneGraphDelta {
2120            new_graph: self.scene_graph.clone(),
2121            invalidates_ids: false,
2122            new_objects: new_object_ids,
2123            exec_outcome: outcome,
2124        };
2125        Ok((src_delta, scene_graph_delta))
2126    }
2127
2128    fn edit_point(
2129        &mut self,
2130        new_ast: &mut ast::Node<ast::Program>,
2131        sketch: ObjectId,
2132        point: ObjectId,
2133        ctor: PointCtor,
2134    ) -> Result<(), KclError> {
2135        // Create updated KCL source from args.
2136        let new_at_ast = to_ast_point2d(&ctor.position).map_err(|err| KclError::refactor(err.to_string()))?;
2137
2138        // Look up existing sketch.
2139        let sketch_id = sketch;
2140        let sketch_object = self
2141            .scene_graph
2142            .objects
2143            .get(sketch_id.0)
2144            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2145        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2146            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2147        };
2148        sketch.segments.iter().find(|o| **o == point).ok_or_else(|| {
2149            KclError::refactor(format!("Point not found in sketch: point={point:?}, sketch={sketch:?}"))
2150        })?;
2151        // Look up existing point.
2152        let point_id = point;
2153        let point_object = self
2154            .scene_graph
2155            .objects
2156            .get(point_id.0)
2157            .ok_or_else(|| KclError::refactor(format!("Point not found in scene graph: point={point:?}")))?;
2158        let ObjectKind::Segment {
2159            segment: Segment::Point(point),
2160        } = &point_object.kind
2161        else {
2162            return Err(KclError::refactor(format!(
2163                "Object is not a point segment: {point_object:?}"
2164            )));
2165        };
2166
2167        // If the point is part of a line or arc, edit the line/arc instead.
2168        if let Some(owner_id) = point.owner {
2169            let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
2170                KclError::refactor(format!(
2171                    "Internal: Owner of point not found in scene graph: owner={owner_id:?}",
2172                ))
2173            })?;
2174            let ObjectKind::Segment { segment } = &owner_object.kind else {
2175                return Err(KclError::refactor(format!(
2176                    "Internal: Owner of point is not a segment, but found {}",
2177                    owner_object.kind.human_friendly_kind_with_article()
2178                )));
2179            };
2180
2181            // Handle Line owner
2182            if let Segment::Line(line) = segment {
2183                let SegmentCtor::Line(line_ctor) = &line.ctor else {
2184                    return Err(KclError::refactor(format!(
2185                        "Internal: Owner of point does not have line ctor, but found {}",
2186                        line.ctor.human_friendly_kind_with_article()
2187                    )));
2188                };
2189                let mut line_ctor = line_ctor.clone();
2190                // Which end of the line is this point?
2191                if line.start == point_id {
2192                    line_ctor.start = ctor.position;
2193                } else if line.end == point_id {
2194                    line_ctor.end = ctor.position;
2195                } else {
2196                    return Err(KclError::refactor(format!(
2197                        "Internal: Point is not part of owner's line segment: point={point_id:?}, line={owner_id:?}"
2198                    )));
2199                }
2200                return self.edit_line(new_ast, sketch_id, owner_id, line_ctor);
2201            }
2202
2203            // Handle Arc owner
2204            if let Segment::Arc(arc) = segment {
2205                let SegmentCtor::Arc(arc_ctor) = &arc.ctor else {
2206                    return Err(KclError::refactor(format!(
2207                        "Internal: Owner of point does not have arc ctor, but found {}",
2208                        arc.ctor.human_friendly_kind_with_article()
2209                    )));
2210                };
2211                let mut arc_ctor = arc_ctor.clone();
2212                // Which point of the arc is this? (center, start, or end)
2213                if arc.center == point_id {
2214                    arc_ctor.center = ctor.position;
2215                } else if arc.start == point_id {
2216                    arc_ctor.start = ctor.position;
2217                } else if arc.end == point_id {
2218                    arc_ctor.end = ctor.position;
2219                } else {
2220                    return Err(KclError::refactor(format!(
2221                        "Internal: Point is not part of owner's arc segment: point={point_id:?}, arc={owner_id:?}"
2222                    )));
2223                }
2224                return self.edit_arc(new_ast, sketch_id, owner_id, arc_ctor);
2225            }
2226
2227            // Handle Circle owner
2228            if let Segment::Circle(circle) = segment {
2229                let SegmentCtor::Circle(circle_ctor) = &circle.ctor else {
2230                    return Err(KclError::refactor(format!(
2231                        "Internal: Owner of point does not have circle ctor, but found {}",
2232                        circle.ctor.human_friendly_kind_with_article()
2233                    )));
2234                };
2235                let mut circle_ctor = circle_ctor.clone();
2236                if circle.center == point_id {
2237                    circle_ctor.center = ctor.position;
2238                } else if circle.start == point_id {
2239                    circle_ctor.start = ctor.position;
2240                } else {
2241                    return Err(KclError::refactor(format!(
2242                        "Internal: Point is not part of owner's circle segment: point={point_id:?}, circle={owner_id:?}"
2243                    )));
2244                }
2245                return self.edit_circle(new_ast, sketch_id, owner_id, circle_ctor);
2246            }
2247
2248            // If owner is neither Line, Arc, nor Circle, allow editing the point directly
2249            // (fall through to the point editing logic below)
2250        }
2251
2252        // Modify the point AST.
2253        self.mutate_ast(new_ast, point_id, AstMutateCommand::EditPoint { at: new_at_ast })?;
2254        Ok(())
2255    }
2256
2257    fn edit_line(
2258        &mut self,
2259        new_ast: &mut ast::Node<ast::Program>,
2260        sketch: ObjectId,
2261        line: ObjectId,
2262        ctor: LineCtor,
2263    ) -> Result<(), KclError> {
2264        // Create updated KCL source from args.
2265        let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2266        let new_end_ast = to_ast_point2d(&ctor.end).map_err(|err| KclError::refactor(err.to_string()))?;
2267
2268        // Look up existing sketch.
2269        let sketch_id = sketch;
2270        let sketch_object = self
2271            .scene_graph
2272            .objects
2273            .get(sketch_id.0)
2274            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2275        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2276            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2277        };
2278        sketch
2279            .segments
2280            .iter()
2281            .find(|o| **o == line)
2282            .ok_or_else(|| KclError::refactor(format!("Line not found in sketch: line={line:?}, sketch={sketch:?}")))?;
2283        // Look up existing line.
2284        let line_id = line;
2285        let line_object = self
2286            .scene_graph
2287            .objects
2288            .get(line_id.0)
2289            .ok_or_else(|| KclError::refactor(format!("Line not found in scene graph: line={line:?}")))?;
2290        let ObjectKind::Segment { .. } = &line_object.kind else {
2291            let kind = line_object.kind.human_friendly_kind_with_article();
2292            return Err(KclError::refactor(format!(
2293                "This constraint only works on Segments, but you selected {kind}"
2294            )));
2295        };
2296
2297        // Modify the line AST.
2298        self.mutate_ast(
2299            new_ast,
2300            line_id,
2301            AstMutateCommand::EditLine {
2302                start: new_start_ast,
2303                end: new_end_ast,
2304                construction: ctor.construction,
2305            },
2306        )?;
2307        Ok(())
2308    }
2309
2310    fn edit_arc(
2311        &mut self,
2312        new_ast: &mut ast::Node<ast::Program>,
2313        sketch: ObjectId,
2314        arc: ObjectId,
2315        ctor: ArcCtor,
2316    ) -> Result<(), KclError> {
2317        // Create updated KCL source from args.
2318        let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2319        let new_end_ast = to_ast_point2d(&ctor.end).map_err(|err| KclError::refactor(err.to_string()))?;
2320        let new_center_ast = to_ast_point2d(&ctor.center).map_err(|err| KclError::refactor(err.to_string()))?;
2321
2322        // Look up existing sketch.
2323        let sketch_id = sketch;
2324        let sketch_object = self
2325            .scene_graph
2326            .objects
2327            .get(sketch_id.0)
2328            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2329        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2330            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2331        };
2332        sketch
2333            .segments
2334            .iter()
2335            .find(|o| **o == arc)
2336            .ok_or_else(|| KclError::refactor(format!("Arc not found in sketch: arc={arc:?}, sketch={sketch:?}")))?;
2337        // Look up existing arc.
2338        let arc_id = arc;
2339        let arc_object = self
2340            .scene_graph
2341            .objects
2342            .get(arc_id.0)
2343            .ok_or_else(|| KclError::refactor(format!("Arc not found in scene graph: arc={arc:?}")))?;
2344        let ObjectKind::Segment { .. } = &arc_object.kind else {
2345            return Err(KclError::refactor(format!("Object is not a segment: {arc_object:?}")));
2346        };
2347
2348        // Modify the arc AST.
2349        self.mutate_ast(
2350            new_ast,
2351            arc_id,
2352            AstMutateCommand::EditArc {
2353                start: new_start_ast,
2354                end: new_end_ast,
2355                center: new_center_ast,
2356                construction: ctor.construction,
2357            },
2358        )?;
2359        Ok(())
2360    }
2361
2362    fn edit_circle(
2363        &mut self,
2364        new_ast: &mut ast::Node<ast::Program>,
2365        sketch: ObjectId,
2366        circle: ObjectId,
2367        ctor: CircleCtor,
2368    ) -> Result<(), KclError> {
2369        // Create updated KCL source from args.
2370        let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2371        let new_center_ast = to_ast_point2d(&ctor.center).map_err(|err| KclError::refactor(err.to_string()))?;
2372
2373        // Look up existing sketch.
2374        let sketch_id = sketch;
2375        let sketch_object = self
2376            .scene_graph
2377            .objects
2378            .get(sketch_id.0)
2379            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2380        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2381            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2382        };
2383        sketch.segments.iter().find(|o| **o == circle).ok_or_else(|| {
2384            KclError::refactor(format!(
2385                "Circle not found in sketch: circle={circle:?}, sketch={sketch:?}"
2386            ))
2387        })?;
2388        // Look up existing circle.
2389        let circle_id = circle;
2390        let circle_object = self
2391            .scene_graph
2392            .objects
2393            .get(circle_id.0)
2394            .ok_or_else(|| KclError::refactor(format!("Circle not found in scene graph: circle={circle:?}")))?;
2395        let ObjectKind::Segment { .. } = &circle_object.kind else {
2396            return Err(KclError::refactor(format!(
2397                "Object is not a segment: {circle_object:?}"
2398            )));
2399        };
2400
2401        // Modify the circle AST.
2402        self.mutate_ast(
2403            new_ast,
2404            circle_id,
2405            AstMutateCommand::EditCircle {
2406                start: new_start_ast,
2407                center: new_center_ast,
2408                construction: ctor.construction,
2409            },
2410        )?;
2411        Ok(())
2412    }
2413
2414    fn delete_segment(
2415        &mut self,
2416        new_ast: &mut ast::Node<ast::Program>,
2417        sketch: ObjectId,
2418        segment_id: ObjectId,
2419    ) -> Result<(), KclError> {
2420        // Look up existing sketch.
2421        let sketch_id = sketch;
2422        let sketch_object = self
2423            .scene_graph
2424            .objects
2425            .get(sketch_id.0)
2426            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2427        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2428            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2429        };
2430        sketch.segments.iter().find(|o| **o == segment_id).ok_or_else(|| {
2431            KclError::refactor(format!(
2432                "Segment not found in sketch: segment={segment_id:?}, sketch={sketch:?}"
2433            ))
2434        })?;
2435        // Look up existing segment.
2436        let segment_object =
2437            self.scene_graph.objects.get(segment_id.0).ok_or_else(|| {
2438                KclError::refactor(format!("Segment not found in scene graph: segment={segment_id:?}"))
2439            })?;
2440        let ObjectKind::Segment { .. } = &segment_object.kind else {
2441            return Err(KclError::refactor(format!(
2442                "Object is not a segment, it is {}",
2443                segment_object.kind.human_friendly_kind_with_article()
2444            )));
2445        };
2446
2447        // Modify the AST to remove the segment.
2448        self.mutate_ast(new_ast, segment_id, AstMutateCommand::DeleteNode)?;
2449        Ok(())
2450    }
2451
2452    fn delete_constraint(
2453        &mut self,
2454        new_ast: &mut ast::Node<ast::Program>,
2455        sketch: ObjectId,
2456        constraint_id: ObjectId,
2457    ) -> Result<(), KclError> {
2458        // Look up existing sketch.
2459        let sketch_id = sketch;
2460        let sketch_object = self
2461            .scene_graph
2462            .objects
2463            .get(sketch_id.0)
2464            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2465        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2466            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2467        };
2468        sketch
2469            .constraints
2470            .iter()
2471            .find(|o| **o == constraint_id)
2472            .ok_or_else(|| {
2473                KclError::refactor(format!(
2474                    "Constraint not found in sketch: constraint={constraint_id:?}, sketch={sketch:?}"
2475                ))
2476            })?;
2477        // Look up existing constraint.
2478        let constraint_object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
2479            KclError::refactor(format!(
2480                "Constraint not found in scene graph: constraint={constraint_id:?}"
2481            ))
2482        })?;
2483        let ObjectKind::Constraint { .. } = &constraint_object.kind else {
2484            return Err(KclError::refactor(format!(
2485                "Object is not a constraint, it is {}",
2486                constraint_object.kind.human_friendly_kind_with_article()
2487            )));
2488        };
2489
2490        // Modify the AST to remove the constraint.
2491        self.mutate_ast(new_ast, constraint_id, AstMutateCommand::DeleteNode)?;
2492        Ok(())
2493    }
2494
2495    fn edit_coincident_constraint(
2496        &mut self,
2497        new_ast: &mut ast::Node<ast::Program>,
2498        constraint_id: ObjectId,
2499        segments: Vec<ConstraintSegment>,
2500    ) -> Result<(), KclError> {
2501        if segments.len() < 2 {
2502            return Err(KclError::refactor(format!(
2503                "Coincident constraint must have at least 2 inputs, got {}",
2504                segments.len()
2505            )));
2506        }
2507
2508        let segment_asts = segments
2509            .iter()
2510            .map(|segment| self.coincident_segment_to_ast(segment, new_ast))
2511            .collect::<Result<Vec<_>, _>>()?;
2512
2513        let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
2514            elements: segment_asts,
2515            digest: None,
2516            non_code_meta: Default::default(),
2517        })));
2518
2519        self.mutate_ast(
2520            new_ast,
2521            constraint_id,
2522            AstMutateCommand::EditCallUnlabeled { arg: array_expr },
2523        )?;
2524        Ok(())
2525    }
2526
2527    /// updates the equalLength constraint with the given lines
2528    fn edit_equal_length_constraint(
2529        &mut self,
2530        new_ast: &mut ast::Node<ast::Program>,
2531        constraint_id: ObjectId,
2532        lines: Vec<ObjectId>,
2533    ) -> Result<(), KclError> {
2534        if lines.len() < 2 {
2535            return Err(KclError::refactor(format!(
2536                "Lines equal length constraint must have at least 2 lines, got {}",
2537                lines.len()
2538            )));
2539        }
2540
2541        let line_asts = lines
2542            .iter()
2543            .map(|line_id| {
2544                let line_object = self
2545                    .scene_graph
2546                    .objects
2547                    .get(line_id.0)
2548                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
2549                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
2550                    let kind = line_object.kind.human_friendly_kind_with_article();
2551                    return Err(KclError::refactor(format!(
2552                        "This constraint only works on Segments, but you selected {kind}"
2553                    )));
2554                };
2555                let Segment::Line(_) = line_segment else {
2556                    let kind = line_segment.human_friendly_kind_with_article();
2557                    return Err(KclError::refactor(format!(
2558                        "Only lines can be made equal length, but you selected {kind}"
2559                    )));
2560                };
2561
2562                get_or_insert_ast_reference(new_ast, &line_object.source.clone(), "line", None)
2563            })
2564            .collect::<Result<Vec<_>, _>>()?;
2565
2566        let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
2567            elements: line_asts,
2568            digest: None,
2569            non_code_meta: Default::default(),
2570        })));
2571
2572        self.mutate_ast(
2573            new_ast,
2574            constraint_id,
2575            AstMutateCommand::EditCallUnlabeled { arg: array_expr },
2576        )?;
2577        Ok(())
2578    }
2579
2580    /// Updates the parallel constraint with the given lines.
2581    fn edit_parallel_constraint(
2582        &mut self,
2583        new_ast: &mut ast::Node<ast::Program>,
2584        constraint_id: ObjectId,
2585        lines: Vec<ObjectId>,
2586    ) -> Result<(), KclError> {
2587        if lines.len() < 2 {
2588            return Err(KclError::refactor(format!(
2589                "Parallel constraint must have at least 2 lines, got {}",
2590                lines.len()
2591            )));
2592        }
2593
2594        let line_asts = lines
2595            .iter()
2596            .map(|line_id| {
2597                let line_object = self
2598                    .scene_graph
2599                    .objects
2600                    .get(line_id.0)
2601                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
2602                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
2603                    let kind = line_object.kind.human_friendly_kind_with_article();
2604                    return Err(KclError::refactor(format!(
2605                        "This constraint only works on Segments, but you selected {kind}"
2606                    )));
2607                };
2608                let Segment::Line(_) = line_segment else {
2609                    let kind = line_segment.human_friendly_kind_with_article();
2610                    return Err(KclError::refactor(format!(
2611                        "Only lines can be made parallel, but you selected {kind}"
2612                    )));
2613                };
2614
2615                get_or_insert_ast_reference(new_ast, &line_object.source.clone(), "line", None)
2616            })
2617            .collect::<Result<Vec<_>, _>>()?;
2618
2619        let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
2620            elements: line_asts,
2621            digest: None,
2622            non_code_meta: Default::default(),
2623        })));
2624
2625        self.mutate_ast(
2626            new_ast,
2627            constraint_id,
2628            AstMutateCommand::EditCallUnlabeled { arg: array_expr },
2629        )?;
2630        Ok(())
2631    }
2632
2633    /// Updates the equalRadius constraint with the given segments.
2634    fn edit_equal_radius_constraint(
2635        &mut self,
2636        new_ast: &mut ast::Node<ast::Program>,
2637        constraint_id: ObjectId,
2638        input: Vec<ObjectId>,
2639    ) -> Result<(), KclError> {
2640        if input.len() < 2 {
2641            return Err(KclError::refactor(format!(
2642                "equalRadius constraint must have at least 2 segments, got {}",
2643                input.len()
2644            )));
2645        }
2646
2647        let input_asts = input
2648            .iter()
2649            .map(|segment_id| self.equal_radius_segment_id_to_ast_reference(*segment_id, new_ast))
2650            .collect::<Result<Vec<_>, _>>()?;
2651
2652        let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
2653            elements: input_asts,
2654            digest: None,
2655            non_code_meta: Default::default(),
2656        })));
2657
2658        self.mutate_ast(
2659            new_ast,
2660            constraint_id,
2661            AstMutateCommand::EditCallUnlabeled { arg: array_expr },
2662        )?;
2663        Ok(())
2664    }
2665
2666    async fn execute_after_edit(
2667        &mut self,
2668        ctx: &ExecutorContext,
2669        sketch: ObjectId,
2670        sketch_block_ref: AstNodeRef,
2671        segment_ids_edited: AhashIndexSet<ObjectId>,
2672        edit_kind: EditDeleteKind,
2673        new_ast: &mut ast::Node<ast::Program>,
2674    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2675        // Convert to string source to create real source ranges.
2676        let new_source = source_from_ast(new_ast);
2677        // Parse the new KCL source.
2678        let (new_program, errors) = Program::parse(&new_source)
2679            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2680        if !errors.is_empty() {
2681            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2682                "Error parsing KCL source after editing: {errors:?}"
2683            ))));
2684        }
2685        let Some(new_program) = new_program else {
2686            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
2687                "No AST produced after editing".to_string(),
2688            )));
2689        };
2690
2691        // TODO: sketch-api: make sure to only set this if there are no errors.
2692        self.program = new_program.clone();
2693
2694        // Truncate after the sketch block for mock execution.
2695        let is_delete = edit_kind.is_delete();
2696        let truncated_program = {
2697            let mut truncated_program = new_program;
2698            only_sketch_block(
2699                &mut truncated_program.ast,
2700                &sketch_block_ref,
2701                edit_kind.to_change_kind(),
2702            )
2703            .map_err(KclErrorWithOutputs::no_outputs)?;
2704            truncated_program
2705        };
2706
2707        #[cfg(not(feature = "artifact-graph"))]
2708        drop(segment_ids_edited);
2709
2710        // Execute.
2711        let mock_config = MockConfig {
2712            sketch_block_id: Some(sketch),
2713            freedom_analysis: is_delete,
2714            #[cfg(feature = "artifact-graph")]
2715            segment_ids_edited: segment_ids_edited.clone(),
2716            ..Default::default()
2717        };
2718        let outcome = ctx.run_mock(&truncated_program, &mock_config).await?;
2719
2720        // Uses freedom_analysis: is_delete
2721        let outcome = self.update_state_after_exec(outcome, is_delete);
2722
2723        #[cfg(feature = "artifact-graph")]
2724        let new_source = {
2725            // Feed back sketch var solutions into the source.
2726            //
2727            // The interpreter is returning all var solutions from the sketch
2728            // block we're editing.
2729            let mut new_ast = self.program.ast.clone();
2730            for (var_range, value) in &outcome.var_solutions {
2731                let rounded = value.round(3);
2732                mutate_ast_node_by_source_range(
2733                    &mut new_ast,
2734                    *var_range,
2735                    AstMutateCommand::EditVarInitialValue { value: rounded },
2736                )
2737                .map_err(|err| KclErrorWithOutputs::from_error_outcome(err, outcome.clone()))?;
2738            }
2739            source_from_ast(&new_ast)
2740        };
2741
2742        let src_delta = SourceDelta { text: new_source };
2743        let scene_graph_delta = SceneGraphDelta {
2744            new_graph: self.scene_graph.clone(),
2745            invalidates_ids: is_delete,
2746            new_objects: Vec::new(),
2747            exec_outcome: outcome,
2748        };
2749        Ok((src_delta, scene_graph_delta))
2750    }
2751
2752    async fn execute_after_delete_sketch(
2753        &mut self,
2754        ctx: &ExecutorContext,
2755        new_ast: &mut ast::Node<ast::Program>,
2756    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2757        // Convert to string source to create real source ranges.
2758        let new_source = source_from_ast(new_ast);
2759        // Parse the new KCL source.
2760        let (new_program, errors) = Program::parse(&new_source)
2761            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2762        if !errors.is_empty() {
2763            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2764                "Error parsing KCL source after editing: {errors:?}"
2765            ))));
2766        }
2767        let Some(new_program) = new_program else {
2768            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
2769                "No AST produced after editing".to_string(),
2770            )));
2771        };
2772
2773        // Make sure to only set this if there are no errors.
2774        self.program = new_program.clone();
2775
2776        // We deleted the entire sketch block. It doesn't make sense to truncate
2777        // and execute only the sketch block. We execute the whole program with
2778        // a real engine.
2779
2780        // Execute.
2781        let outcome = ctx.run_with_caching(new_program).await?;
2782        let freedom_analysis_ran = true;
2783
2784        let outcome = self.update_state_after_exec(outcome, freedom_analysis_ran);
2785
2786        let src_delta = SourceDelta { text: new_source };
2787        let scene_graph_delta = SceneGraphDelta {
2788            new_graph: self.scene_graph.clone(),
2789            invalidates_ids: true,
2790            new_objects: Vec::new(),
2791            exec_outcome: outcome,
2792        };
2793        Ok((src_delta, scene_graph_delta))
2794    }
2795
2796    /// Map a point object id into an AST reference expression for use in
2797    /// constraints. If the point is owned by a segment (line or arc), we
2798    /// reference the appropriate property on that segment (e.g. `line1.start`,
2799    /// `arc1.center`). Otherwise we reference the point directly.
2800    fn point_id_to_ast_reference(
2801        &self,
2802        point_id: ObjectId,
2803        new_ast: &mut ast::Node<ast::Program>,
2804    ) -> Result<ast::Expr, KclError> {
2805        let point_object = self
2806            .scene_graph
2807            .objects
2808            .get(point_id.0)
2809            .ok_or_else(|| KclError::refactor(format!("Point not found: {point_id:?}")))?;
2810        let ObjectKind::Segment { segment: point_segment } = &point_object.kind else {
2811            return Err(KclError::refactor(format!("Object is not a segment: {point_object:?}")));
2812        };
2813        let Segment::Point(point) = point_segment else {
2814            return Err(KclError::refactor(format!(
2815                "Only points are currently supported: {point_object:?}"
2816            )));
2817        };
2818
2819        if let Some(owner_id) = point.owner {
2820            let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
2821                KclError::refactor(format!(
2822                    "Owner of point not found in scene graph: point={point_id:?}, owner={owner_id:?}"
2823                ))
2824            })?;
2825            let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind else {
2826                return Err(KclError::refactor(format!(
2827                    "Owner of point is not a segment, but found {}",
2828                    owner_object.kind.human_friendly_kind_with_article()
2829                )));
2830            };
2831
2832            match owner_segment {
2833                Segment::Line(line) => {
2834                    let property = if line.start == point_id {
2835                        LINE_PROPERTY_START
2836                    } else if line.end == point_id {
2837                        LINE_PROPERTY_END
2838                    } else {
2839                        return Err(KclError::refactor(format!(
2840                            "Internal: Point is not part of owner's line segment: point={point_id:?}, line={owner_id:?}"
2841                        )));
2842                    };
2843                    get_or_insert_ast_reference(new_ast, &owner_object.source, "line", Some(property))
2844                }
2845                Segment::Arc(arc) => {
2846                    let property = if arc.start == point_id {
2847                        ARC_PROPERTY_START
2848                    } else if arc.end == point_id {
2849                        ARC_PROPERTY_END
2850                    } else if arc.center == point_id {
2851                        ARC_PROPERTY_CENTER
2852                    } else {
2853                        return Err(KclError::refactor(format!(
2854                            "Internal: Point is not part of owner's arc segment: point={point_id:?}, arc={owner_id:?}"
2855                        )));
2856                    };
2857                    get_or_insert_ast_reference(new_ast, &owner_object.source, "arc", Some(property))
2858                }
2859                Segment::Circle(circle) => {
2860                    let property = if circle.start == point_id {
2861                        CIRCLE_PROPERTY_START
2862                    } else if circle.center == point_id {
2863                        CIRCLE_PROPERTY_CENTER
2864                    } else {
2865                        return Err(KclError::refactor(format!(
2866                            "Internal: Point is not part of owner's circle segment: point={point_id:?}, circle={owner_id:?}"
2867                        )));
2868                    };
2869                    get_or_insert_ast_reference(new_ast, &owner_object.source, CIRCLE_VARIABLE, Some(property))
2870                }
2871                _ => Err(KclError::refactor(format!(
2872                    "Internal: Owner of point is not a supported segment type for constraints: {owner_segment:?}"
2873                ))),
2874            }
2875        } else {
2876            // Standalone point.
2877            get_or_insert_ast_reference(new_ast, &point_object.source, "point", None)
2878        }
2879    }
2880
2881    fn coincident_segment_to_ast(
2882        &self,
2883        segment: &ConstraintSegment,
2884        new_ast: &mut ast::Node<ast::Program>,
2885    ) -> Result<ast::Expr, KclError> {
2886        match segment {
2887            ConstraintSegment::Origin(_) => Ok(ast_name_expr("ORIGIN".to_owned())),
2888            ConstraintSegment::Segment(segment_id) => {
2889                let segment_object = self
2890                    .scene_graph
2891                    .objects
2892                    .get(segment_id.0)
2893                    .ok_or_else(|| KclError::refactor(format!("Object not found: {segment_id:?}")))?;
2894                let ObjectKind::Segment { segment } = &segment_object.kind else {
2895                    return Err(KclError::refactor(format!(
2896                        "Object is not a segment, it is {}",
2897                        segment_object.kind.human_friendly_kind_with_article()
2898                    )));
2899                };
2900
2901                match segment {
2902                    Segment::Point(_) => self.point_id_to_ast_reference(*segment_id, new_ast),
2903                    Segment::Line(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, "line", None),
2904                    Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, "arc", None),
2905                    Segment::Circle(_) => {
2906                        get_or_insert_ast_reference(new_ast, &segment_object.source, CIRCLE_VARIABLE, None)
2907                    }
2908                }
2909            }
2910        }
2911    }
2912
2913    fn axis_constraint_segment_to_ast(
2914        &self,
2915        segment: &ConstraintSegment,
2916        new_ast: &mut ast::Node<ast::Program>,
2917    ) -> Result<ast::Expr, KclError> {
2918        match segment {
2919            ConstraintSegment::Origin(_) => Ok(ast_name_expr("ORIGIN".to_owned())),
2920            ConstraintSegment::Segment(point_id) => self.point_id_to_ast_reference(*point_id, new_ast),
2921        }
2922    }
2923
2924    async fn add_coincident(
2925        &mut self,
2926        sketch: ObjectId,
2927        coincident: Coincident,
2928        new_ast: &mut ast::Node<ast::Program>,
2929    ) -> Result<AstNodeRef, KclError> {
2930        let sketch_id = sketch;
2931        let segment_asts = coincident
2932            .segments
2933            .iter()
2934            .map(|segment| self.coincident_segment_to_ast(segment, new_ast))
2935            .collect::<Result<Vec<_>, _>>()?;
2936        if segment_asts.len() < 2 {
2937            return Err(KclError::refactor(format!(
2938                "Coincident constraint must have at least 2 inputs, got {}",
2939                segment_asts.len()
2940            )));
2941        }
2942
2943        // Create the coincident() call using shared helper.
2944        let coincident_ast = create_coincident_ast(segment_asts);
2945
2946        // Add the line to the AST of the sketch block.
2947        let (sketch_block_ref, _) = self.mutate_ast(
2948            new_ast,
2949            sketch_id,
2950            AstMutateCommand::AddSketchBlockExprStmt { expr: coincident_ast },
2951        )?;
2952        Ok(sketch_block_ref)
2953    }
2954
2955    async fn add_distance(
2956        &mut self,
2957        sketch: ObjectId,
2958        distance: Distance,
2959        new_ast: &mut ast::Node<ast::Program>,
2960    ) -> Result<AstNodeRef, KclError> {
2961        let sketch_id = sketch;
2962        let [pt0_ast, pt1_ast] = match distance.points.as_slice() {
2963            [pt0, pt1] => [
2964                self.coincident_segment_to_ast(pt0, new_ast)?,
2965                self.coincident_segment_to_ast(pt1, new_ast)?,
2966            ],
2967            _ => {
2968                return Err(KclError::refactor(format!(
2969                    "Distance constraint must have exactly 2 points, got {}",
2970                    distance.points.len()
2971                )));
2972            }
2973        };
2974
2975        // Create the distance() call.
2976        let distance_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
2977            callee: ast::Node::no_src(ast_sketch2_name(DISTANCE_FN)),
2978            unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
2979                ast::ArrayExpression {
2980                    elements: vec![pt0_ast, pt1_ast],
2981                    digest: None,
2982                    non_code_meta: Default::default(),
2983                },
2984            )))),
2985            arguments: Default::default(),
2986            digest: None,
2987            non_code_meta: Default::default(),
2988        })));
2989        let distance_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
2990            left: distance_call_ast,
2991            operator: ast::BinaryOperator::Eq,
2992            right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
2993                value: ast::LiteralValue::Number {
2994                    value: distance.distance.value,
2995                    suffix: distance.distance.units,
2996                },
2997                raw: format_number_literal(distance.distance.value, distance.distance.units, None).map_err(|_| {
2998                    KclError::refactor(format!(
2999                        "Could not format numeric suffix: {:?}",
3000                        distance.distance.units
3001                    ))
3002                })?,
3003                digest: None,
3004            }))),
3005            digest: None,
3006        })));
3007
3008        // Add the line to the AST of the sketch block.
3009        let (sketch_block_ref, _) = self.mutate_ast(
3010            new_ast,
3011            sketch_id,
3012            AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
3013        )?;
3014        Ok(sketch_block_ref)
3015    }
3016
3017    async fn add_angle(
3018        &mut self,
3019        sketch: ObjectId,
3020        angle: Angle,
3021        new_ast: &mut ast::Node<ast::Program>,
3022    ) -> Result<AstNodeRef, KclError> {
3023        let &[l0_id, l1_id] = angle.lines.as_slice() else {
3024            return Err(KclError::refactor(format!(
3025                "Angle constraint must have exactly 2 lines, got {}",
3026                angle.lines.len()
3027            )));
3028        };
3029        let sketch_id = sketch;
3030
3031        // Map the runtime objects back to variable names.
3032        let line0_object = self
3033            .scene_graph
3034            .objects
3035            .get(l0_id.0)
3036            .ok_or_else(|| KclError::refactor(format!("Line not found: {l0_id:?}")))?;
3037        let ObjectKind::Segment { segment: line0_segment } = &line0_object.kind else {
3038            return Err(KclError::refactor(format!("Object is not a segment: {line0_object:?}")));
3039        };
3040        let Segment::Line(_) = line0_segment else {
3041            return Err(KclError::refactor(format!(
3042                "Only lines can be constrained to meet at an angle: {line0_object:?}",
3043            )));
3044        };
3045        let l0_ast = get_or_insert_ast_reference(new_ast, &line0_object.source.clone(), "line", None)?;
3046
3047        let line1_object = self
3048            .scene_graph
3049            .objects
3050            .get(l1_id.0)
3051            .ok_or_else(|| KclError::refactor(format!("Line not found: {l1_id:?}")))?;
3052        let ObjectKind::Segment { segment: line1_segment } = &line1_object.kind else {
3053            return Err(KclError::refactor(format!("Object is not a segment: {line1_object:?}")));
3054        };
3055        let Segment::Line(_) = line1_segment else {
3056            return Err(KclError::refactor(format!(
3057                "Only lines can be constrained to meet at an angle: {line1_object:?}",
3058            )));
3059        };
3060        let l1_ast = get_or_insert_ast_reference(new_ast, &line1_object.source.clone(), "line", None)?;
3061
3062        // Create the angle() call.
3063        let angle_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3064            callee: ast::Node::no_src(ast_sketch2_name(ANGLE_FN)),
3065            unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
3066                ast::ArrayExpression {
3067                    elements: vec![l0_ast, l1_ast],
3068                    digest: None,
3069                    non_code_meta: Default::default(),
3070                },
3071            )))),
3072            arguments: Default::default(),
3073            digest: None,
3074            non_code_meta: Default::default(),
3075        })));
3076        let angle_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
3077            left: angle_call_ast,
3078            operator: ast::BinaryOperator::Eq,
3079            right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
3080                value: ast::LiteralValue::Number {
3081                    value: angle.angle.value,
3082                    suffix: angle.angle.units,
3083                },
3084                raw: format_number_literal(angle.angle.value, angle.angle.units, None).map_err(|_| {
3085                    KclError::refactor(format!("Could not format numeric suffix: {:?}", angle.angle.units))
3086                })?,
3087                digest: None,
3088            }))),
3089            digest: None,
3090        })));
3091
3092        // Add the line to the AST of the sketch block.
3093        let (sketch_block_ref, _) = self.mutate_ast(
3094            new_ast,
3095            sketch_id,
3096            AstMutateCommand::AddSketchBlockExprStmt { expr: angle_ast },
3097        )?;
3098        Ok(sketch_block_ref)
3099    }
3100
3101    async fn add_tangent(
3102        &mut self,
3103        sketch: ObjectId,
3104        tangent: Tangent,
3105        new_ast: &mut ast::Node<ast::Program>,
3106    ) -> Result<AstNodeRef, KclError> {
3107        let &[seg0_id, seg1_id] = tangent.input.as_slice() else {
3108            return Err(KclError::refactor(format!(
3109                "Tangent constraint must have exactly 2 segments, got {}",
3110                tangent.input.len()
3111            )));
3112        };
3113        let sketch_id = sketch;
3114
3115        let seg0_object = self
3116            .scene_graph
3117            .objects
3118            .get(seg0_id.0)
3119            .ok_or_else(|| KclError::refactor(format!("Segment not found: {seg0_id:?}")))?;
3120        let ObjectKind::Segment { segment: seg0_segment } = &seg0_object.kind else {
3121            return Err(KclError::refactor(format!("Object is not a segment: {seg0_object:?}")));
3122        };
3123        let seg0_ast = match seg0_segment {
3124            Segment::Line(_) => get_or_insert_ast_reference(new_ast, &seg0_object.source, "line", None)?,
3125            Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &seg0_object.source, "arc", None)?,
3126            Segment::Circle(_) => get_or_insert_ast_reference(new_ast, &seg0_object.source, CIRCLE_VARIABLE, None)?,
3127            _ => {
3128                return Err(KclError::refactor(format!(
3129                    "Tangent supports only line/arc/circle segments, got: {seg0_segment:?}"
3130                )));
3131            }
3132        };
3133
3134        let seg1_object = self
3135            .scene_graph
3136            .objects
3137            .get(seg1_id.0)
3138            .ok_or_else(|| KclError::refactor(format!("Segment not found: {seg1_id:?}")))?;
3139        let ObjectKind::Segment { segment: seg1_segment } = &seg1_object.kind else {
3140            return Err(KclError::refactor(format!("Object is not a segment: {seg1_object:?}")));
3141        };
3142        let seg1_ast = match seg1_segment {
3143            Segment::Line(_) => get_or_insert_ast_reference(new_ast, &seg1_object.source, "line", None)?,
3144            Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &seg1_object.source, "arc", None)?,
3145            Segment::Circle(_) => get_or_insert_ast_reference(new_ast, &seg1_object.source, CIRCLE_VARIABLE, None)?,
3146            _ => {
3147                return Err(KclError::refactor(format!(
3148                    "Tangent supports only line/arc/circle segments, got: {seg1_segment:?}"
3149                )));
3150            }
3151        };
3152
3153        let tangent_ast = create_tangent_ast(seg0_ast, seg1_ast);
3154        let (sketch_block_ref, _) = self.mutate_ast(
3155            new_ast,
3156            sketch_id,
3157            AstMutateCommand::AddSketchBlockExprStmt { expr: tangent_ast },
3158        )?;
3159        Ok(sketch_block_ref)
3160    }
3161
3162    async fn add_midpoint(
3163        &mut self,
3164        sketch: ObjectId,
3165        midpoint: Midpoint,
3166        new_ast: &mut ast::Node<ast::Program>,
3167    ) -> Result<AstNodeRef, KclError> {
3168        let sketch_id = sketch;
3169        let point_ast = self.point_id_to_ast_reference(midpoint.point, new_ast)?;
3170
3171        let segment_object = self
3172            .scene_graph
3173            .objects
3174            .get(midpoint.segment.0)
3175            .ok_or_else(|| KclError::refactor(format!("Segment not found: {:?}", midpoint.segment)))?;
3176        let ObjectKind::Segment {
3177            segment: midpoint_segment,
3178        } = &segment_object.kind
3179        else {
3180            return Err(KclError::refactor(format!(
3181                "Object must be a segment, but it was {}",
3182                segment_object.kind.human_friendly_kind_with_article()
3183            )));
3184        };
3185        let segment_ast = match midpoint_segment {
3186            Segment::Line(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, "line", None)?,
3187            Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, "arc", None)?,
3188            _ => {
3189                return Err(KclError::refactor(format!(
3190                    "Midpoint target must be a line or arc segment but it was {}",
3191                    midpoint_segment.human_friendly_kind_with_article()
3192                )));
3193            }
3194        };
3195
3196        let midpoint_ast = create_midpoint_ast(segment_ast, point_ast);
3197        let (sketch_block_ref, _) = self.mutate_ast(
3198            new_ast,
3199            sketch_id,
3200            AstMutateCommand::AddSketchBlockExprStmt { expr: midpoint_ast },
3201        )?;
3202        Ok(sketch_block_ref)
3203    }
3204
3205    async fn add_equal_radius(
3206        &mut self,
3207        sketch: ObjectId,
3208        equal_radius: EqualRadius,
3209        new_ast: &mut ast::Node<ast::Program>,
3210    ) -> Result<AstNodeRef, KclError> {
3211        if equal_radius.input.len() < 2 {
3212            return Err(KclError::refactor(format!(
3213                "equalRadius constraint must have at least 2 segments, got {}",
3214                equal_radius.input.len()
3215            )));
3216        }
3217
3218        let sketch_id = sketch;
3219        let input_asts = equal_radius
3220            .input
3221            .iter()
3222            .map(|segment_id| self.equal_radius_segment_id_to_ast_reference(*segment_id, new_ast))
3223            .collect::<Result<Vec<_>, _>>()?;
3224
3225        let equal_radius_ast = create_equal_radius_ast(input_asts);
3226        let (sketch_block_ref, _) = self.mutate_ast(
3227            new_ast,
3228            sketch_id,
3229            AstMutateCommand::AddSketchBlockExprStmt { expr: equal_radius_ast },
3230        )?;
3231        Ok(sketch_block_ref)
3232    }
3233
3234    async fn add_radius(
3235        &mut self,
3236        sketch: ObjectId,
3237        radius: Radius,
3238        new_ast: &mut ast::Node<ast::Program>,
3239    ) -> Result<AstNodeRef, KclError> {
3240        let params = ArcSizeConstraintParams {
3241            points: vec![radius.arc],
3242            function_name: RADIUS_FN,
3243            value: radius.radius.value,
3244            units: radius.radius.units,
3245            constraint_type_name: "Radius",
3246        };
3247        self.add_arc_size_constraint(sketch, params, new_ast).await
3248    }
3249
3250    async fn add_diameter(
3251        &mut self,
3252        sketch: ObjectId,
3253        diameter: Diameter,
3254        new_ast: &mut ast::Node<ast::Program>,
3255    ) -> Result<AstNodeRef, KclError> {
3256        let params = ArcSizeConstraintParams {
3257            points: vec![diameter.arc],
3258            function_name: DIAMETER_FN,
3259            value: diameter.diameter.value,
3260            units: diameter.diameter.units,
3261            constraint_type_name: "Diameter",
3262        };
3263        self.add_arc_size_constraint(sketch, params, new_ast).await
3264    }
3265
3266    async fn add_fixed_constraints(
3267        &mut self,
3268        sketch: ObjectId,
3269        points: Vec<FixedPoint>,
3270        new_ast: &mut ast::Node<ast::Program>,
3271    ) -> Result<AstNodeRef, KclError> {
3272        let mut sketch_block_ref = None;
3273
3274        for fixed_point in points {
3275            let point_ast = self.point_id_to_ast_reference(fixed_point.point, new_ast)?;
3276            let fixed_ast = create_fixed_point_constraint_ast(point_ast, fixed_point.position)
3277                .map_err(|err| KclError::refactor(err.to_string()))?;
3278
3279            let (sketch_ref, _) = self.mutate_ast(
3280                new_ast,
3281                sketch,
3282                AstMutateCommand::AddSketchBlockExprStmt { expr: fixed_ast },
3283            )?;
3284            sketch_block_ref = Some(sketch_ref);
3285        }
3286
3287        sketch_block_ref.ok_or_else(|| KclError::refactor("Fixed constraint requires at least one point".to_owned()))
3288    }
3289
3290    async fn add_arc_size_constraint(
3291        &mut self,
3292        sketch: ObjectId,
3293        params: ArcSizeConstraintParams,
3294        new_ast: &mut ast::Node<ast::Program>,
3295    ) -> Result<AstNodeRef, KclError> {
3296        let sketch_id = sketch;
3297
3298        // Constraint must have exactly 1 argument (arc segment)
3299        if params.points.len() != 1 {
3300            return Err(KclError::refactor(format!(
3301                "{} constraint must have exactly 1 argument (an arc segment), got {}",
3302                params.constraint_type_name,
3303                params.points.len()
3304            )));
3305        }
3306
3307        let arc_id = params.points[0];
3308        let arc_object = self
3309            .scene_graph
3310            .objects
3311            .get(arc_id.0)
3312            .ok_or_else(|| KclError::refactor(format!("Arc segment not found: {arc_id:?}")))?;
3313        let ObjectKind::Segment { segment: arc_segment } = &arc_object.kind else {
3314            return Err(KclError::refactor(format!("Object is not a segment: {arc_object:?}")));
3315        };
3316        let ref_type = match arc_segment {
3317            Segment::Arc(_) => "arc",
3318            Segment::Circle(_) => CIRCLE_VARIABLE,
3319            _ => {
3320                return Err(KclError::refactor(format!(
3321                    "{} constraint argument must be an arc or circle segment, got: {arc_segment:?}",
3322                    params.constraint_type_name
3323                )));
3324            }
3325        };
3326        // Reference the arc/circle segment directly
3327        let arc_ast = get_or_insert_ast_reference(new_ast, &arc_object.source, ref_type, None)?;
3328
3329        // Create the function call.
3330        let call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3331            callee: ast::Node::no_src(ast_sketch2_name(params.function_name)),
3332            unlabeled: Some(arc_ast),
3333            arguments: Default::default(),
3334            digest: None,
3335            non_code_meta: Default::default(),
3336        })));
3337        let constraint_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
3338            left: call_ast,
3339            operator: ast::BinaryOperator::Eq,
3340            right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
3341                value: ast::LiteralValue::Number {
3342                    value: params.value,
3343                    suffix: params.units,
3344                },
3345                raw: format_number_literal(params.value, params.units, None)
3346                    .map_err(|_| KclError::refactor(format!("Could not format numeric suffix: {:?}", params.units)))?,
3347                digest: None,
3348            }))),
3349            digest: None,
3350        })));
3351
3352        // Add the line to the AST of the sketch block.
3353        let (sketch_block_ref, _) = self.mutate_ast(
3354            new_ast,
3355            sketch_id,
3356            AstMutateCommand::AddSketchBlockExprStmt { expr: constraint_ast },
3357        )?;
3358        Ok(sketch_block_ref)
3359    }
3360
3361    async fn add_horizontal_distance(
3362        &mut self,
3363        sketch: ObjectId,
3364        distance: Distance,
3365        new_ast: &mut ast::Node<ast::Program>,
3366    ) -> Result<AstNodeRef, KclError> {
3367        let sketch_id = sketch;
3368        let [pt0_ast, pt1_ast] = match distance.points.as_slice() {
3369            [pt0, pt1] => [
3370                self.coincident_segment_to_ast(pt0, new_ast)?,
3371                self.coincident_segment_to_ast(pt1, new_ast)?,
3372            ],
3373            _ => {
3374                return Err(KclError::refactor(format!(
3375                    "Horizontal distance constraint must have exactly 2 points, got {}",
3376                    distance.points.len()
3377                )));
3378            }
3379        };
3380
3381        // Create the horizontalDistance() call.
3382        let distance_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3383            callee: ast::Node::no_src(ast_sketch2_name(HORIZONTAL_DISTANCE_FN)),
3384            unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
3385                ast::ArrayExpression {
3386                    elements: vec![pt0_ast, pt1_ast],
3387                    digest: None,
3388                    non_code_meta: Default::default(),
3389                },
3390            )))),
3391            arguments: Default::default(),
3392            digest: None,
3393            non_code_meta: Default::default(),
3394        })));
3395        let distance_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
3396            left: distance_call_ast,
3397            operator: ast::BinaryOperator::Eq,
3398            right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
3399                value: ast::LiteralValue::Number {
3400                    value: distance.distance.value,
3401                    suffix: distance.distance.units,
3402                },
3403                raw: format_number_literal(distance.distance.value, distance.distance.units, None).map_err(|_| {
3404                    KclError::refactor(format!(
3405                        "Could not format numeric suffix: {:?}",
3406                        distance.distance.units
3407                    ))
3408                })?,
3409                digest: None,
3410            }))),
3411            digest: None,
3412        })));
3413
3414        // Add the line to the AST of the sketch block.
3415        let (sketch_block_ref, _) = self.mutate_ast(
3416            new_ast,
3417            sketch_id,
3418            AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
3419        )?;
3420        Ok(sketch_block_ref)
3421    }
3422
3423    async fn add_vertical_distance(
3424        &mut self,
3425        sketch: ObjectId,
3426        distance: Distance,
3427        new_ast: &mut ast::Node<ast::Program>,
3428    ) -> Result<AstNodeRef, KclError> {
3429        let sketch_id = sketch;
3430        let [pt0_ast, pt1_ast] = match distance.points.as_slice() {
3431            [pt0, pt1] => [
3432                self.coincident_segment_to_ast(pt0, new_ast)?,
3433                self.coincident_segment_to_ast(pt1, new_ast)?,
3434            ],
3435            _ => {
3436                return Err(KclError::refactor(format!(
3437                    "Vertical distance constraint must have exactly 2 points, got {}",
3438                    distance.points.len()
3439                )));
3440            }
3441        };
3442
3443        // Create the verticalDistance() call.
3444        let distance_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3445            callee: ast::Node::no_src(ast_sketch2_name(VERTICAL_DISTANCE_FN)),
3446            unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
3447                ast::ArrayExpression {
3448                    elements: vec![pt0_ast, pt1_ast],
3449                    digest: None,
3450                    non_code_meta: Default::default(),
3451                },
3452            )))),
3453            arguments: Default::default(),
3454            digest: None,
3455            non_code_meta: Default::default(),
3456        })));
3457        let distance_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
3458            left: distance_call_ast,
3459            operator: ast::BinaryOperator::Eq,
3460            right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
3461                value: ast::LiteralValue::Number {
3462                    value: distance.distance.value,
3463                    suffix: distance.distance.units,
3464                },
3465                raw: format_number_literal(distance.distance.value, distance.distance.units, None).map_err(|_| {
3466                    KclError::refactor(format!(
3467                        "Could not format numeric suffix: {:?}",
3468                        distance.distance.units
3469                    ))
3470                })?,
3471                digest: None,
3472            }))),
3473            digest: None,
3474        })));
3475
3476        // Add the line to the AST of the sketch block.
3477        let (sketch_block_ref, _) = self.mutate_ast(
3478            new_ast,
3479            sketch_id,
3480            AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
3481        )?;
3482        Ok(sketch_block_ref)
3483    }
3484
3485    async fn add_horizontal(
3486        &mut self,
3487        sketch: ObjectId,
3488        horizontal: Horizontal,
3489        new_ast: &mut ast::Node<ast::Program>,
3490    ) -> Result<AstNodeRef, KclError> {
3491        let sketch_id = sketch;
3492
3493        // Map the runtime objects back to variable names.
3494        let first_arg_ast = match horizontal {
3495            Horizontal::Line { line } => {
3496                let line_object = self
3497                    .scene_graph
3498                    .objects
3499                    .get(line.0)
3500                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line:?}")))?;
3501                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3502                    let kind = line_object.kind.human_friendly_kind_with_article();
3503                    return Err(KclError::refactor(format!(
3504                        "This constraint only works on Segments, but you selected {kind}"
3505                    )));
3506                };
3507                let Segment::Line(_) = line_segment else {
3508                    return Err(KclError::refactor(format!(
3509                        "Only lines can be made horizontal, but you selected {}",
3510                        line_segment.human_friendly_kind_with_article(),
3511                    )));
3512                };
3513                get_or_insert_ast_reference(new_ast, &line_object.source.clone(), "line", None)?
3514            }
3515            Horizontal::Points { points } => {
3516                let point_asts = points
3517                    .iter()
3518                    .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
3519                    .collect::<Result<Vec<_>, _>>()?;
3520                ast::ArrayExpression::new(point_asts).into()
3521            }
3522        };
3523
3524        // Create the horizontal() call using shared helper.
3525        let horizontal_ast = create_horizontal_ast(first_arg_ast);
3526
3527        // Add the line to the AST of the sketch block.
3528        let (sketch_block_ref, _) = self.mutate_ast(
3529            new_ast,
3530            sketch_id,
3531            AstMutateCommand::AddSketchBlockExprStmt { expr: horizontal_ast },
3532        )?;
3533        Ok(sketch_block_ref)
3534    }
3535
3536    async fn add_lines_equal_length(
3537        &mut self,
3538        sketch: ObjectId,
3539        lines_equal_length: LinesEqualLength,
3540        new_ast: &mut ast::Node<ast::Program>,
3541    ) -> Result<AstNodeRef, KclError> {
3542        if lines_equal_length.lines.len() < 2 {
3543            return Err(KclError::refactor(format!(
3544                "Lines equal length constraint must have at least 2 lines, got {}",
3545                lines_equal_length.lines.len()
3546            )));
3547        };
3548
3549        let sketch_id = sketch;
3550
3551        // Map the runtime objects back to variable names.
3552        let line_asts = lines_equal_length
3553            .lines
3554            .iter()
3555            .map(|line_id| {
3556                let line_object = self
3557                    .scene_graph
3558                    .objects
3559                    .get(line_id.0)
3560                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3561                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3562                    let kind = line_object.kind.human_friendly_kind_with_article();
3563                    return Err(KclError::refactor(format!(
3564                        "This constraint only works on Segments, but you selected {kind}"
3565                    )));
3566                };
3567                let Segment::Line(_) = line_segment else {
3568                    let kind = line_segment.human_friendly_kind_with_article();
3569                    return Err(KclError::refactor(format!(
3570                        "Only lines can be made equal length, but you selected {kind}"
3571                    )));
3572                };
3573
3574                get_or_insert_ast_reference(new_ast, &line_object.source.clone(), "line", None)
3575            })
3576            .collect::<Result<Vec<_>, _>>()?;
3577
3578        // Create the equalLength() call using shared helper.
3579        let equal_length_ast = create_equal_length_ast(line_asts);
3580
3581        // Add the constraint to the AST of the sketch block.
3582        let (sketch_block_ref, _) = self.mutate_ast(
3583            new_ast,
3584            sketch_id,
3585            AstMutateCommand::AddSketchBlockExprStmt { expr: equal_length_ast },
3586        )?;
3587        Ok(sketch_block_ref)
3588    }
3589
3590    fn equal_radius_segment_id_to_ast_reference(
3591        &mut self,
3592        segment_id: ObjectId,
3593        new_ast: &mut ast::Node<ast::Program>,
3594    ) -> Result<ast::Expr, KclError> {
3595        let segment_object = self
3596            .scene_graph
3597            .objects
3598            .get(segment_id.0)
3599            .ok_or_else(|| KclError::refactor(format!("Segment not found: {segment_id:?}")))?;
3600        let ObjectKind::Segment { segment } = &segment_object.kind else {
3601            return Err(KclError::refactor(format!(
3602                "Object is not a segment, it was {}",
3603                segment_object.kind.human_friendly_kind_with_article()
3604            )));
3605        };
3606
3607        let ref_type = match segment {
3608            Segment::Arc(_) => "arc",
3609            Segment::Circle(_) => CIRCLE_VARIABLE,
3610            _ => {
3611                return Err(KclError::refactor(format!(
3612                    "equalRadius supports only arc/circle segments, got {}",
3613                    segment.human_friendly_kind_with_article()
3614                )));
3615            }
3616        };
3617
3618        get_or_insert_ast_reference(new_ast, &segment_object.source, ref_type, None)
3619    }
3620
3621    async fn add_parallel(
3622        &mut self,
3623        sketch: ObjectId,
3624        parallel: Parallel,
3625        new_ast: &mut ast::Node<ast::Program>,
3626    ) -> Result<AstNodeRef, KclError> {
3627        if parallel.lines.len() < 2 {
3628            return Err(KclError::refactor(format!(
3629                "Parallel constraint must have at least 2 lines, got {}",
3630                parallel.lines.len()
3631            )));
3632        };
3633
3634        let sketch_id = sketch;
3635
3636        let line_asts = parallel
3637            .lines
3638            .iter()
3639            .map(|line_id| {
3640                let line_object = self
3641                    .scene_graph
3642                    .objects
3643                    .get(line_id.0)
3644                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3645                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3646                    let kind = line_object.kind.human_friendly_kind_with_article();
3647                    return Err(KclError::refactor(format!(
3648                        "This constraint only works on Segments, but you selected {kind}"
3649                    )));
3650                };
3651                let Segment::Line(_) = line_segment else {
3652                    let kind = line_segment.human_friendly_kind_with_article();
3653                    return Err(KclError::refactor(format!(
3654                        "Only lines can be made parallel, but you selected {kind}"
3655                    )));
3656                };
3657
3658                get_or_insert_ast_reference(new_ast, &line_object.source.clone(), "line", None)
3659            })
3660            .collect::<Result<Vec<_>, _>>()?;
3661
3662        let call_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3663            callee: ast::Node::no_src(ast_sketch2_name(LinesAtAngleKind::Parallel.to_function_name())),
3664            unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
3665                ast::ArrayExpression {
3666                    elements: line_asts,
3667                    digest: None,
3668                    non_code_meta: Default::default(),
3669                },
3670            )))),
3671            arguments: Default::default(),
3672            digest: None,
3673            non_code_meta: Default::default(),
3674        })));
3675
3676        let (sketch_block_ref, _) = self.mutate_ast(
3677            new_ast,
3678            sketch_id,
3679            AstMutateCommand::AddSketchBlockExprStmt { expr: call_ast },
3680        )?;
3681        Ok(sketch_block_ref)
3682    }
3683
3684    async fn add_perpendicular(
3685        &mut self,
3686        sketch: ObjectId,
3687        perpendicular: Perpendicular,
3688        new_ast: &mut ast::Node<ast::Program>,
3689    ) -> Result<AstNodeRef, KclError> {
3690        self.add_lines_at_angle_constraint(sketch, LinesAtAngleKind::Perpendicular, perpendicular.lines, new_ast)
3691            .await
3692    }
3693
3694    async fn add_lines_at_angle_constraint(
3695        &mut self,
3696        sketch: ObjectId,
3697        angle_kind: LinesAtAngleKind,
3698        lines: Vec<ObjectId>,
3699        new_ast: &mut ast::Node<ast::Program>,
3700    ) -> Result<AstNodeRef, KclError> {
3701        let &[line0_id, line1_id] = lines.as_slice() else {
3702            return Err(KclError::refactor(format!(
3703                "{} constraint must have exactly 2 lines, got {}",
3704                angle_kind.to_function_name(),
3705                lines.len()
3706            )));
3707        };
3708
3709        let sketch_id = sketch;
3710
3711        // Map the runtime objects back to variable names.
3712        let line0_object = self
3713            .scene_graph
3714            .objects
3715            .get(line0_id.0)
3716            .ok_or_else(|| KclError::refactor(format!("Line not found: {line0_id:?}")))?;
3717        let ObjectKind::Segment { segment: line0_segment } = &line0_object.kind else {
3718            let kind = line0_object.kind.human_friendly_kind_with_article();
3719            return Err(KclError::refactor(format!(
3720                "This constraint only works on Segments, but you selected {kind}"
3721            )));
3722        };
3723        let Segment::Line(_) = line0_segment else {
3724            return Err(KclError::refactor(format!(
3725                "Only lines can be made {}, but you selected {}",
3726                angle_kind.to_function_name(),
3727                line0_segment.human_friendly_kind_with_article(),
3728            )));
3729        };
3730        let line0_ast = get_or_insert_ast_reference(new_ast, &line0_object.source.clone(), "line", None)?;
3731
3732        let line1_object = self
3733            .scene_graph
3734            .objects
3735            .get(line1_id.0)
3736            .ok_or_else(|| KclError::refactor(format!("Line not found: {line1_id:?}")))?;
3737        let ObjectKind::Segment { segment: line1_segment } = &line1_object.kind else {
3738            let kind = line1_object.kind.human_friendly_kind_with_article();
3739            return Err(KclError::refactor(format!(
3740                "This constraint only works on Segments, but you selected {kind}"
3741            )));
3742        };
3743        let Segment::Line(_) = line1_segment else {
3744            return Err(KclError::refactor(format!(
3745                "Only lines can be made {}, but you selected {}",
3746                angle_kind.to_function_name(),
3747                line1_segment.human_friendly_kind_with_article(),
3748            )));
3749        };
3750        let line1_ast = get_or_insert_ast_reference(new_ast, &line1_object.source.clone(), "line", None)?;
3751
3752        // Create the parallel() or perpendicular() call.
3753        let call_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3754            callee: ast::Node::no_src(ast_sketch2_name(angle_kind.to_function_name())),
3755            unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
3756                ast::ArrayExpression {
3757                    elements: vec![line0_ast, line1_ast],
3758                    digest: None,
3759                    non_code_meta: Default::default(),
3760                },
3761            )))),
3762            arguments: Default::default(),
3763            digest: None,
3764            non_code_meta: Default::default(),
3765        })));
3766
3767        // Add the constraint to the AST of the sketch block.
3768        let (sketch_block_ref, _) = self.mutate_ast(
3769            new_ast,
3770            sketch_id,
3771            AstMutateCommand::AddSketchBlockExprStmt { expr: call_ast },
3772        )?;
3773        Ok(sketch_block_ref)
3774    }
3775
3776    async fn add_vertical(
3777        &mut self,
3778        sketch: ObjectId,
3779        vertical: Vertical,
3780        new_ast: &mut ast::Node<ast::Program>,
3781    ) -> Result<AstNodeRef, KclError> {
3782        let sketch_id = sketch;
3783
3784        let first_arg_ast = match vertical {
3785            Vertical::Line { line } => {
3786                // Map the runtime objects back to variable names.
3787                let line_object = self
3788                    .scene_graph
3789                    .objects
3790                    .get(line.0)
3791                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line:?}")))?;
3792                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3793                    let kind = line_object.kind.human_friendly_kind_with_article();
3794                    return Err(KclError::refactor(format!(
3795                        "This constraint only works on Segments, but you selected {kind}"
3796                    )));
3797                };
3798                let Segment::Line(_) = line_segment else {
3799                    return Err(KclError::refactor(format!(
3800                        "Only lines can be made vertical, but you selected {}",
3801                        line_segment.human_friendly_kind_with_article()
3802                    )));
3803                };
3804                get_or_insert_ast_reference(new_ast, &line_object.source.clone(), "line", None)?
3805            }
3806            Vertical::Points { points } => {
3807                let point_asts = points
3808                    .iter()
3809                    .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
3810                    .collect::<Result<Vec<_>, _>>()?;
3811                ast::ArrayExpression::new(point_asts).into()
3812            }
3813        };
3814
3815        // Create the vertical() call using shared helper.
3816        let vertical_ast = create_vertical_ast(first_arg_ast);
3817
3818        // Add the line to the AST of the sketch block.
3819        let (sketch_block_ref, _) = self.mutate_ast(
3820            new_ast,
3821            sketch_id,
3822            AstMutateCommand::AddSketchBlockExprStmt { expr: vertical_ast },
3823        )?;
3824        Ok(sketch_block_ref)
3825    }
3826
3827    async fn execute_after_add_constraint(
3828        &mut self,
3829        ctx: &ExecutorContext,
3830        sketch_id: ObjectId,
3831        #[cfg_attr(not(feature = "artifact-graph"), allow(unused_variables))] sketch_block_ref: AstNodeRef,
3832        new_ast: &mut ast::Node<ast::Program>,
3833    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
3834        // Convert to string source to create real source ranges.
3835        let new_source = source_from_ast(new_ast);
3836        // Parse the new KCL source.
3837        let (new_program, errors) = Program::parse(&new_source)
3838            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
3839        if !errors.is_empty() {
3840            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
3841                "Error parsing KCL source after adding constraint: {errors:?}"
3842            ))));
3843        }
3844        let Some(new_program) = new_program else {
3845            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
3846                "No AST produced after adding constraint".to_string(),
3847            )));
3848        };
3849        #[cfg(feature = "artifact-graph")]
3850        let constraint_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
3851            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
3852                "Source range of new constraint not found in sketch block: {sketch_block_ref:?}; {err:?}"
3853            )))
3854        })?;
3855
3856        // Truncate after the sketch block for mock execution.
3857        // Use a clone so we don't mutate new_program yet
3858        let mut truncated_program = new_program.clone();
3859        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
3860            .map_err(KclErrorWithOutputs::no_outputs)?;
3861
3862        // Execute - if this fails, we haven't modified self yet, so state is safe
3863        let outcome = ctx
3864            .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch_id))
3865            .await?;
3866
3867        #[cfg(not(feature = "artifact-graph"))]
3868        let new_object_ids = Vec::new();
3869        #[cfg(feature = "artifact-graph")]
3870        let new_object_ids = {
3871            // Extract the constraint ID from the execution outcome using source_range_to_object
3872            let constraint_id = outcome
3873                .source_range_to_object
3874                .get(&constraint_node_ref.range)
3875                .copied()
3876                .ok_or_else(|| {
3877                    KclErrorWithOutputs::from_error_outcome(
3878                        KclError::refactor(format!("Source range of constraint not found: {constraint_node_ref:?}")),
3879                        outcome.clone(),
3880                    )
3881                })?;
3882            vec![constraint_id]
3883        };
3884
3885        // Only now, after all operations succeeded, update self.program
3886        // This ensures state is only modified if everything succeeds
3887        self.program = new_program;
3888
3889        // Uses MockConfig::default() which has freedom_analysis: true
3890        let outcome = self.update_state_after_exec(outcome, true);
3891
3892        let src_delta = SourceDelta { text: new_source };
3893        let scene_graph_delta = SceneGraphDelta {
3894            new_graph: self.scene_graph.clone(),
3895            invalidates_ids: false,
3896            new_objects: new_object_ids,
3897            exec_outcome: outcome,
3898        };
3899        Ok((src_delta, scene_graph_delta))
3900    }
3901
3902    // Find constraints that reference the given segments.
3903    fn find_referenced_constraints(
3904        &self,
3905        sketch_id: ObjectId,
3906        segment_ids_set: &AhashIndexSet<ObjectId>,
3907    ) -> Result<AhashIndexSet<ObjectId>, KclError> {
3908        // Look up the sketch.
3909        let sketch_object = self
3910            .scene_graph
3911            .objects
3912            .get(sketch_id.0)
3913            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch_id:?}")))?;
3914        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
3915            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
3916        };
3917        let mut constraint_ids_set = AhashIndexSet::default();
3918        for constraint_id in &sketch.constraints {
3919            let constraint_object = self
3920                .scene_graph
3921                .objects
3922                .get(constraint_id.0)
3923                .ok_or_else(|| KclError::refactor(format!("Constraint not found: {constraint_id:?}")))?;
3924            let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
3925                return Err(KclError::refactor(format!(
3926                    "Object is not a constraint, it is {}",
3927                    constraint_object.kind.human_friendly_kind_with_article()
3928                )));
3929            };
3930            let depends_on_segment = match constraint {
3931                Constraint::Coincident(c) => c.segment_ids().any(|seg_id| {
3932                    // Check if the segment itself is being deleted
3933                    if segment_ids_set.contains(&seg_id) {
3934                        return true;
3935                    }
3936                    // For points, also check if the owner line/arc is being deleted
3937                    let seg_object = self.scene_graph.objects.get(seg_id.0);
3938                    if let Some(obj) = seg_object
3939                        && let ObjectKind::Segment { segment } = &obj.kind
3940                        && let Segment::Point(pt) = segment
3941                        && let Some(owner_line_id) = pt.owner
3942                    {
3943                        return segment_ids_set.contains(&owner_line_id);
3944                    }
3945                    false
3946                }),
3947                Constraint::Distance(d) => d.point_ids().any(|pt_id| {
3948                    if segment_ids_set.contains(&pt_id) {
3949                        return true;
3950                    }
3951                    let pt_object = self.scene_graph.objects.get(pt_id.0);
3952                    if let Some(obj) = pt_object
3953                        && let ObjectKind::Segment { segment } = &obj.kind
3954                        && let Segment::Point(pt) = segment
3955                        && let Some(owner_line_id) = pt.owner
3956                    {
3957                        return segment_ids_set.contains(&owner_line_id);
3958                    }
3959                    false
3960                }),
3961                Constraint::Fixed(_) => false,
3962                Constraint::Radius(r) => segment_ids_set.contains(&r.arc),
3963                Constraint::Diameter(d) => segment_ids_set.contains(&d.arc),
3964                Constraint::EqualRadius(equal_radius) => {
3965                    equal_radius.input.iter().any(|seg_id| segment_ids_set.contains(seg_id))
3966                }
3967                Constraint::HorizontalDistance(d) => d.point_ids().any(|pt_id| {
3968                    let pt_object = self.scene_graph.objects.get(pt_id.0);
3969                    if let Some(obj) = pt_object
3970                        && let ObjectKind::Segment { segment } = &obj.kind
3971                        && let Segment::Point(pt) = segment
3972                        && let Some(owner_line_id) = pt.owner
3973                    {
3974                        return segment_ids_set.contains(&owner_line_id);
3975                    }
3976                    false
3977                }),
3978                Constraint::VerticalDistance(d) => d.point_ids().any(|pt_id| {
3979                    let pt_object = self.scene_graph.objects.get(pt_id.0);
3980                    if let Some(obj) = pt_object
3981                        && let ObjectKind::Segment { segment } = &obj.kind
3982                        && let Segment::Point(pt) = segment
3983                        && let Some(owner_line_id) = pt.owner
3984                    {
3985                        return segment_ids_set.contains(&owner_line_id);
3986                    }
3987                    false
3988                }),
3989                Constraint::Horizontal(h) => match h {
3990                    Horizontal::Line { line } => segment_ids_set.contains(line),
3991                    Horizontal::Points { points } => points.iter().any(|point| match point {
3992                        ConstraintSegment::Segment(point) => segment_ids_set.contains(point),
3993                        ConstraintSegment::Origin(_) => false,
3994                    }),
3995                },
3996                Constraint::Vertical(v) => match v {
3997                    Vertical::Line { line } => segment_ids_set.contains(line),
3998                    Vertical::Points { points } => points.iter().any(|point| match point {
3999                        ConstraintSegment::Segment(point) => segment_ids_set.contains(point),
4000                        ConstraintSegment::Origin(_) => false,
4001                    }),
4002                },
4003                Constraint::LinesEqualLength(lines_equal_length) => lines_equal_length
4004                    .lines
4005                    .iter()
4006                    .any(|line_id| segment_ids_set.contains(line_id)),
4007                Constraint::Midpoint(midpoint) => {
4008                    segment_ids_set.contains(&midpoint.segment)
4009                        || segment_ids_set.contains(&midpoint.point)
4010                        || self
4011                            .scene_graph
4012                            .objects
4013                            .get(midpoint.point.0)
4014                            .and_then(|obj| match &obj.kind {
4015                                ObjectKind::Segment {
4016                                    segment: Segment::Point(point),
4017                                } => point.owner,
4018                                _ => None,
4019                            })
4020                            .is_some_and(|owner_id| segment_ids_set.contains(&owner_id))
4021                }
4022                Constraint::Parallel(parallel) => {
4023                    parallel.lines.iter().any(|line_id| segment_ids_set.contains(line_id))
4024                }
4025                Constraint::Perpendicular(perpendicular) => perpendicular
4026                    .lines
4027                    .iter()
4028                    .any(|line_id| segment_ids_set.contains(line_id)),
4029                Constraint::Angle(angle) => angle.lines.iter().any(|line_id| segment_ids_set.contains(line_id)),
4030                Constraint::Tangent(tangent) => tangent.input.iter().any(|seg_id| segment_ids_set.contains(seg_id)),
4031            };
4032            if depends_on_segment {
4033                constraint_ids_set.insert(*constraint_id);
4034            }
4035        }
4036        Ok(constraint_ids_set)
4037    }
4038
4039    fn update_state_after_exec(&mut self, outcome: ExecOutcome, freedom_analysis_ran: bool) -> ExecOutcome {
4040        #[cfg(not(feature = "artifact-graph"))]
4041        {
4042            let _ = freedom_analysis_ran; // Only used when artifact-graph feature is enabled
4043            outcome
4044        }
4045        #[cfg(feature = "artifact-graph")]
4046        {
4047            let mut outcome = outcome;
4048            let mut new_objects = std::mem::take(&mut outcome.scene_objects);
4049
4050            if freedom_analysis_ran {
4051                // When freedom analysis ran, replace the cache entirely with new values
4052                // Don't merge with old values since IDs might have changed
4053                self.point_freedom_cache.clear();
4054                for new_obj in &new_objects {
4055                    if let ObjectKind::Segment {
4056                        segment: crate::front::Segment::Point(point),
4057                    } = &new_obj.kind
4058                    {
4059                        self.point_freedom_cache.insert(new_obj.id, point.freedom);
4060                    }
4061                }
4062                add_wall_and_cap_face_objects(&mut new_objects, &outcome.artifact_graph);
4063                // Objects are already correct from the analysis, just use them as-is
4064                self.scene_graph.objects = new_objects;
4065            } else {
4066                // When freedom analysis didn't run, preserve old values and merge
4067                // Before replacing objects, extract and store freedom values from old objects
4068                for old_obj in &self.scene_graph.objects {
4069                    if let ObjectKind::Segment {
4070                        segment: crate::front::Segment::Point(point),
4071                    } = &old_obj.kind
4072                    {
4073                        self.point_freedom_cache.insert(old_obj.id, point.freedom);
4074                    }
4075                }
4076
4077                // Update objects, preserving stored freedom values when new is Free (might be default)
4078                let mut updated_objects = Vec::with_capacity(new_objects.len());
4079                for new_obj in new_objects {
4080                    let mut obj = new_obj;
4081                    if let ObjectKind::Segment {
4082                        segment: crate::front::Segment::Point(point),
4083                    } = &mut obj.kind
4084                    {
4085                        let new_freedom = point.freedom;
4086                        // When freedom_analysis=false, new values are defaults (Free).
4087                        // Only preserve cached values when new is Free (indicating it's a default, not from analysis).
4088                        // If new is NOT Free, use the new value (it came from somewhere else, maybe conflict detection).
4089                        // Never preserve Conflict from cache - conflicts are transient and should only be set
4090                        // when there are actually unsatisfied constraints.
4091                        match new_freedom {
4092                            Freedom::Free => {
4093                                match self.point_freedom_cache.get(&obj.id).copied() {
4094                                    Some(Freedom::Conflict) => {
4095                                        // Don't preserve Conflict - conflicts are transient
4096                                        // Keep it as Free
4097                                    }
4098                                    Some(Freedom::Fixed) => {
4099                                        // Preserve Fixed cached value
4100                                        point.freedom = Freedom::Fixed;
4101                                    }
4102                                    Some(Freedom::Free) => {
4103                                        // If stored is also Free, keep Free (no change needed)
4104                                    }
4105                                    None => {
4106                                        // If no cached value, keep Free (default)
4107                                    }
4108                                }
4109                            }
4110                            Freedom::Fixed => {
4111                                // Use new value (already set)
4112                            }
4113                            Freedom::Conflict => {
4114                                // Use new value (already set)
4115                            }
4116                        }
4117                        // Store the new freedom value (even if it's Free, so we know it was set)
4118                        self.point_freedom_cache.insert(obj.id, point.freedom);
4119                    }
4120                    updated_objects.push(obj);
4121                }
4122
4123                add_wall_and_cap_face_objects(&mut updated_objects, &outcome.artifact_graph);
4124                self.scene_graph.objects = updated_objects;
4125            }
4126            outcome
4127        }
4128    }
4129
4130    fn mutate_ast(
4131        &mut self,
4132        ast: &mut ast::Node<ast::Program>,
4133        object_id: ObjectId,
4134        command: AstMutateCommand,
4135    ) -> Result<(AstNodeRef, AstMutateCommandReturn), KclError> {
4136        let sketch_object = self
4137            .scene_graph
4138            .objects
4139            .get(object_id.0)
4140            .ok_or_else(|| KclError::refactor(format!("Object not found: {object_id:?}")))?;
4141        match &sketch_object.source {
4142            SourceRef::Simple { range, node_path: _ } => mutate_ast_node_by_source_range(ast, *range, command),
4143            SourceRef::BackTrace { .. } => {
4144                Err(KclError::refactor("BackTrace source refs not supported yet".to_owned()))
4145            }
4146        }
4147    }
4148}
4149
4150fn sketch_block_ref_from_id(scene_graph: &SceneGraph, sketch_id: ObjectId) -> Result<AstNodeRef, KclError> {
4151    // Look up existing sketch.
4152    let sketch_object = scene_graph
4153        .objects
4154        .get(sketch_id.0)
4155        .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch_id:?}")))?;
4156    let ObjectKind::Sketch(_) = &sketch_object.kind else {
4157        return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
4158    };
4159    expect_single_node_ref(sketch_object)
4160}
4161
4162fn expect_single_node_ref(object: &Object) -> Result<AstNodeRef, KclError> {
4163    match &object.source {
4164        SourceRef::Simple { range, node_path } => Ok(AstNodeRef {
4165            range: *range,
4166            node_path: node_path.clone(),
4167        }),
4168        SourceRef::BackTrace { ranges } => {
4169            let [range] = ranges.as_slice() else {
4170                return Err(KclError::refactor(format!(
4171                    "Expected single location in SourceRef, got {}; ranges={ranges:#?}",
4172                    ranges.len()
4173                )));
4174            };
4175            Ok(AstNodeRef {
4176                range: range.0,
4177                node_path: range.1.clone(),
4178            })
4179        }
4180    }
4181}
4182
4183fn expect_single_source_range(source_ref: &SourceRef) -> Result<SourceRange, KclError> {
4184    match source_ref {
4185        SourceRef::Simple { range, node_path: _ } => Ok(*range),
4186        SourceRef::BackTrace { ranges } => {
4187            if ranges.len() != 1 {
4188                return Err(KclError::refactor(format!(
4189                    "Expected single source range in SourceRef, got {}; ranges={ranges:#?}",
4190                    ranges.len(),
4191                )));
4192            }
4193            Ok(ranges[0].0)
4194        }
4195    }
4196}
4197
4198/// This is a deprecated fall-back implementation. Prefer
4199/// [`only_sketch_block()`] to avoid reliance on source ranges.
4200fn only_sketch_block_from_range(
4201    ast: &mut ast::Node<ast::Program>,
4202    sketch_block_range: SourceRange,
4203    edit_kind: ChangeKind,
4204) -> Result<(), KclError> {
4205    let r1 = sketch_block_range;
4206    let matches_range = |r2: SourceRange| -> bool {
4207        // We may have added items to the sketch block, so the end may not be an
4208        // exact match.
4209        match edit_kind {
4210            ChangeKind::Add => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() <= r2.end(),
4211            // For edit, we don't know whether it grew or shrank.
4212            ChangeKind::Edit => r1.module_id() == r2.module_id() && r1.start() == r2.start(),
4213            ChangeKind::Delete => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() >= r2.end(),
4214            // No edit should be an exact match.
4215            ChangeKind::None => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() == r2.end(),
4216        }
4217    };
4218    let mut found = false;
4219    for item in ast.body.iter_mut() {
4220        match item {
4221            ast::BodyItem::ImportStatement(_) => {}
4222            ast::BodyItem::ExpressionStatement(node) => {
4223                if matches_range(SourceRange::from(&*node))
4224                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.expression
4225                {
4226                    sketch_block.is_being_edited = true;
4227                    found = true;
4228                    break;
4229                }
4230            }
4231            ast::BodyItem::VariableDeclaration(node) => {
4232                if matches_range(SourceRange::from(&node.declaration.init))
4233                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.declaration.init
4234                {
4235                    sketch_block.is_being_edited = true;
4236                    found = true;
4237                    break;
4238                }
4239            }
4240            ast::BodyItem::TypeDeclaration(_) => {}
4241            ast::BodyItem::ReturnStatement(node) => {
4242                if matches_range(SourceRange::from(&node.argument))
4243                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.argument
4244                {
4245                    sketch_block.is_being_edited = true;
4246                    found = true;
4247                    break;
4248                }
4249            }
4250        }
4251    }
4252    if !found {
4253        return Err(KclError::refactor(format!(
4254            "Sketch block source range not found in AST: {sketch_block_range:?}, edit_kind={edit_kind:?}"
4255        )));
4256    }
4257
4258    Ok(())
4259}
4260
4261fn only_sketch_block(
4262    ast: &mut ast::Node<ast::Program>,
4263    sketch_block_ref: &AstNodeRef,
4264    edit_kind: ChangeKind,
4265) -> Result<(), KclError> {
4266    let Some(target_node_path) = &sketch_block_ref.node_path else {
4267        #[cfg(target_arch = "wasm32")]
4268        web_sys::console::warn_1(
4269            &format!(
4270                "only_sketch_block: target sketch block ref doesn't have node path; sketch_block_ref={:#?}, edit_kind={edit_kind:#?}",
4271                &sketch_block_ref
4272            )
4273            .into(),
4274        );
4275        return only_sketch_block_from_range(ast, sketch_block_ref.range, edit_kind);
4276    };
4277    let mut found = false;
4278    for item in ast.body.iter_mut() {
4279        match item {
4280            ast::BodyItem::ImportStatement(_) => {}
4281            ast::BodyItem::ExpressionStatement(node) => {
4282                // Check the statement.
4283                if let Some(node_path) = &node.node_path
4284                    && node_path == target_node_path
4285                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.expression
4286                {
4287                    sketch_block.is_being_edited = true;
4288                    found = true;
4289                    break;
4290                }
4291                // Check the expression.
4292                if let Some(node_path) = node.expression.node_path()
4293                    && node_path == target_node_path
4294                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.expression
4295                {
4296                    sketch_block.is_being_edited = true;
4297                    found = true;
4298                    break;
4299                }
4300            }
4301            ast::BodyItem::VariableDeclaration(node) => {
4302                if let Some(node_path) = node.declaration.init.node_path()
4303                    && node_path == target_node_path
4304                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.declaration.init
4305                {
4306                    sketch_block.is_being_edited = true;
4307                    found = true;
4308                    break;
4309                }
4310            }
4311            ast::BodyItem::TypeDeclaration(_) => {}
4312            ast::BodyItem::ReturnStatement(node) => {
4313                if let Some(node_path) = node.argument.node_path()
4314                    && node_path == target_node_path
4315                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.argument
4316                {
4317                    sketch_block.is_being_edited = true;
4318                    found = true;
4319                    break;
4320                }
4321            }
4322        }
4323    }
4324    if !found {
4325        return Err(KclError::refactor(format!(
4326            "Sketch block node path not found in AST: {sketch_block_ref:?}, edit_kind={edit_kind:?}"
4327        )));
4328    }
4329
4330    Ok(())
4331}
4332
4333fn sketch_on_ast_expr(
4334    ast: &mut ast::Node<ast::Program>,
4335    scene_graph: &SceneGraph,
4336    on: &Plane,
4337) -> Result<ast::Expr, KclError> {
4338    match on {
4339        Plane::Default(name) => Ok(default_plane_ast_expr(*name)),
4340        Plane::Object(object_id) => {
4341            let on_object = scene_graph
4342                .objects
4343                .get(object_id.0)
4344                .ok_or_else(|| KclError::refactor(format!("Sketch plane object not found: {object_id:?}")))?;
4345            #[cfg(feature = "artifact-graph")]
4346            {
4347                if let Some(face_expr) = sketch_face_of_scene_object_ast_expr(ast, on_object)? {
4348                    return Ok(face_expr);
4349                }
4350            }
4351            get_or_insert_ast_reference(ast, &on_object.source, "plane", None)
4352        }
4353    }
4354}
4355
4356#[cfg(feature = "artifact-graph")]
4357fn sketch_face_of_scene_object_ast_expr(
4358    ast: &mut ast::Node<ast::Program>,
4359    on_object: &crate::front::Object,
4360) -> Result<Option<ast::Expr>, KclError> {
4361    let SourceRef::BackTrace { ranges } = &on_object.source else {
4362        return Ok(None);
4363    };
4364
4365    match &on_object.kind {
4366        ObjectKind::Wall(_) => {
4367            let [sweep_range, segment_range] = ranges.as_slice() else {
4368                return Err(KclError::refactor(format!(
4369                    "Expected wall source metadata to have 2 ranges, got {}; artifact_id={:?}",
4370                    ranges.len(),
4371                    on_object.artifact_id
4372                )));
4373            };
4374            let sweep_ref = get_or_insert_ast_reference(
4375                ast,
4376                &SourceRef::Simple {
4377                    range: sweep_range.0,
4378                    node_path: sweep_range.1.clone(),
4379                },
4380                "solid",
4381                None,
4382            )?;
4383            let ast::Expr::Name(solid_name_expr) = sweep_ref else {
4384                return Err(KclError::refactor(format!(
4385                    "Could not resolve sweep reference for selected wall: artifact_id={:?}",
4386                    on_object.artifact_id
4387                )));
4388            };
4389            let solid_name = solid_name_expr.name.name.clone();
4390            let solid_expr = ast_name_expr(solid_name.clone());
4391            let segment_ref = get_or_insert_ast_reference(
4392                ast,
4393                &SourceRef::Simple {
4394                    range: segment_range.0,
4395                    node_path: segment_range.1.clone(),
4396                },
4397                "line",
4398                None,
4399            )?;
4400
4401            let face_expr = if let Some(region_name) = region_name_from_sweep_variable(ast, &solid_name) {
4402                let ast::Expr::Name(segment_name_expr) = segment_ref else {
4403                    return Err(KclError::refactor(format!(
4404                        "Could not resolve source segment reference for selected region wall: artifact_id={:?}",
4405                        on_object.artifact_id
4406                    )));
4407                };
4408                create_member_expression(
4409                    create_member_expression(ast_name_expr(region_name), "tags"),
4410                    &segment_name_expr.name.name,
4411                )
4412            } else {
4413                segment_ref
4414            };
4415
4416            Ok(Some(create_face_of_ast(solid_expr, face_expr)))
4417        }
4418        ObjectKind::Cap(cap) => {
4419            let [range] = ranges.as_slice() else {
4420                return Err(KclError::refactor(format!(
4421                    "Expected cap source metadata to have 1 range, got {}; artifact_id={:?}",
4422                    ranges.len(),
4423                    on_object.artifact_id
4424                )));
4425            };
4426            let sweep_ref = get_or_insert_ast_reference(
4427                ast,
4428                &SourceRef::Simple {
4429                    range: range.0,
4430                    node_path: range.1.clone(),
4431                },
4432                "solid",
4433                None,
4434            )?;
4435            let ast::Expr::Name(solid_name_expr) = sweep_ref else {
4436                return Err(KclError::refactor(format!(
4437                    "Could not resolve sweep reference for selected cap: artifact_id={:?}",
4438                    on_object.artifact_id
4439                )));
4440            };
4441            let solid_expr = ast_name_expr(solid_name_expr.name.name.clone());
4442            // TODO: change this to explicit tag references with tagStart/tagEnd mutations
4443            let face_expr = match cap.kind {
4444                crate::frontend::api::CapKind::Start => ast_name_expr("START".to_owned()),
4445                crate::frontend::api::CapKind::End => ast_name_expr("END".to_owned()),
4446            };
4447
4448            Ok(Some(create_face_of_ast(solid_expr, face_expr)))
4449        }
4450        _ => Ok(None),
4451    }
4452}
4453
4454#[cfg(feature = "artifact-graph")]
4455fn add_wall_and_cap_face_objects(scene_objects: &mut Vec<crate::front::Object>, artifact_graph: &ArtifactGraph) {
4456    let mut existing_artifact_ids = scene_objects
4457        .iter()
4458        .map(|object| object.artifact_id)
4459        .collect::<HashSet<_>>();
4460
4461    for artifact in artifact_graph.values() {
4462        match artifact {
4463            Artifact::Wall(wall) => {
4464                if existing_artifact_ids.contains(&wall.id) {
4465                    continue;
4466                }
4467
4468                let Some(segment) = artifact_graph.get(&wall.seg_id).and_then(|artifact| match artifact {
4469                    Artifact::Segment(segment) => Some(segment),
4470                    _ => None,
4471                }) else {
4472                    continue;
4473                };
4474                let Some(sweep) = artifact_graph.get(&wall.sweep_id).and_then(|artifact| match artifact {
4475                    Artifact::Sweep(sweep) => Some(sweep),
4476                    _ => None,
4477                }) else {
4478                    continue;
4479                };
4480                let source_segment = segment
4481                    .original_seg_id
4482                    .and_then(|original_seg_id| artifact_graph.get(&original_seg_id))
4483                    .and_then(|artifact| match artifact {
4484                        Artifact::Segment(segment) => Some(segment),
4485                        _ => None,
4486                    })
4487                    .unwrap_or(segment);
4488                let id = ObjectId(scene_objects.len());
4489                scene_objects.push(crate::front::Object {
4490                    id,
4491                    kind: ObjectKind::Wall(crate::frontend::api::Wall { id }),
4492                    label: Default::default(),
4493                    comments: Default::default(),
4494                    artifact_id: wall.id,
4495                    source: SourceRef::BackTrace {
4496                        ranges: vec![
4497                            (sweep.code_ref.range, Some(sweep.code_ref.node_path.clone())),
4498                            (
4499                                source_segment.code_ref.range,
4500                                Some(source_segment.code_ref.node_path.clone()),
4501                            ),
4502                        ],
4503                    },
4504                });
4505                existing_artifact_ids.insert(wall.id);
4506            }
4507            Artifact::Cap(cap) => {
4508                if existing_artifact_ids.contains(&cap.id) {
4509                    continue;
4510                }
4511
4512                let Some(sweep) = artifact_graph.get(&cap.sweep_id).and_then(|artifact| match artifact {
4513                    Artifact::Sweep(sweep) => Some(sweep),
4514                    _ => None,
4515                }) else {
4516                    continue;
4517                };
4518                let id = ObjectId(scene_objects.len());
4519                let kind = match cap.sub_type {
4520                    CapSubType::Start => crate::frontend::api::CapKind::Start,
4521                    CapSubType::End => crate::frontend::api::CapKind::End,
4522                };
4523                scene_objects.push(crate::front::Object {
4524                    id,
4525                    kind: ObjectKind::Cap(crate::frontend::api::Cap { id, kind }),
4526                    label: Default::default(),
4527                    comments: Default::default(),
4528                    artifact_id: cap.id,
4529                    source: SourceRef::BackTrace {
4530                        ranges: vec![(sweep.code_ref.range, Some(sweep.code_ref.node_path.clone()))],
4531                    },
4532                });
4533                existing_artifact_ids.insert(cap.id);
4534            }
4535            _ => {}
4536        }
4537    }
4538}
4539
4540fn default_plane_ast_expr(name: crate::engine::PlaneName) -> ast::Expr {
4541    use crate::engine::PlaneName;
4542
4543    match name {
4544        PlaneName::Xy => ast_name_expr("XY".to_owned()),
4545        PlaneName::Xz => ast_name_expr("XZ".to_owned()),
4546        PlaneName::Yz => ast_name_expr("YZ".to_owned()),
4547        PlaneName::NegXy => negated_plane_ast_expr("XY"),
4548        PlaneName::NegXz => negated_plane_ast_expr("XZ"),
4549        PlaneName::NegYz => negated_plane_ast_expr("YZ"),
4550    }
4551}
4552
4553fn negated_plane_ast_expr(name: &str) -> ast::Expr {
4554    ast::Expr::UnaryExpression(Box::new(ast::UnaryExpression::new(
4555        ast::UnaryOperator::Neg,
4556        ast::BinaryPart::Name(Box::new(ast_name(name.to_owned()))),
4557    )))
4558}
4559
4560#[cfg(feature = "artifact-graph")]
4561fn create_face_of_ast(solid_expr: ast::Expr, face_expr: ast::Expr) -> ast::Expr {
4562    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
4563        callee: ast::Node::no_src(ast_sketch2_name("faceOf")),
4564        unlabeled: Some(solid_expr),
4565        arguments: vec![ast::LabeledArg {
4566            label: Some(ast::Identifier::new("face")),
4567            arg: face_expr,
4568        }],
4569        digest: None,
4570        non_code_meta: Default::default(),
4571    })))
4572}
4573
4574#[cfg(feature = "artifact-graph")]
4575fn region_name_from_sweep_variable(ast: &ast::Node<ast::Program>, sweep_variable_name: &str) -> Option<String> {
4576    let ast::Definition::Variable(sweep_decl) = ast.get_variable(sweep_variable_name)? else {
4577        return None;
4578    };
4579    let ast::Expr::CallExpressionKw(sweep_call) = &sweep_decl.init else {
4580        return None;
4581    };
4582    if !matches!(
4583        sweep_call.callee.name.name.as_str(),
4584        "extrude" | "revolve" | "sweep" | "loft"
4585    ) {
4586        return None;
4587    }
4588    let ast::Expr::Name(region_name_expr) = sweep_call.unlabeled.as_ref()? else {
4589        return None;
4590    };
4591    let candidate = region_name_expr.name.name.clone();
4592    let ast::Definition::Variable(region_decl) = ast.get_variable(&candidate)? else {
4593        return None;
4594    };
4595    let ast::Expr::CallExpressionKw(region_call) = &region_decl.init else {
4596        return None;
4597    };
4598    if region_call.callee.name.name != "region" {
4599        return None;
4600    }
4601    Some(candidate)
4602}
4603
4604/// Return the AST expression referencing the variable at the given source ref.
4605/// If no such variable exists, insert a new variable declaration with the given
4606/// prefix.
4607///
4608/// This may return a complex expression referencing properties of the variable
4609/// (e.g., `line1.start`).
4610fn get_or_insert_ast_reference(
4611    ast: &mut ast::Node<ast::Program>,
4612    source_ref: &SourceRef,
4613    prefix: &str,
4614    property: Option<&str>,
4615) -> Result<ast::Expr, KclError> {
4616    let range = expect_single_source_range(source_ref)?;
4617    let command = AstMutateCommand::AddVariableDeclaration {
4618        prefix: prefix.to_owned(),
4619    };
4620    let (_, ret) = mutate_ast_node_by_source_range(ast, range, command)?;
4621    let AstMutateCommandReturn::Name(var_name) = ret else {
4622        return Err(KclError::refactor(
4623            "Expected variable name returned from AddVariableDeclaration".to_owned(),
4624        ));
4625    };
4626    let var_expr = ast::Expr::Name(Box::new(ast::Name::new(&var_name)));
4627    let Some(property) = property else {
4628        // No property; just return the variable name.
4629        return Ok(var_expr);
4630    };
4631
4632    Ok(create_member_expression(var_expr, property))
4633}
4634
4635fn mutate_ast_node_by_source_range(
4636    ast: &mut ast::Node<ast::Program>,
4637    source_range: SourceRange,
4638    command: AstMutateCommand,
4639) -> Result<(AstNodeRef, AstMutateCommandReturn), KclError> {
4640    let mut context = AstMutateContext {
4641        source_range,
4642        node_path: None,
4643        command,
4644        defined_names_stack: Default::default(),
4645    };
4646    let control = dfs_mut(ast, &mut context);
4647    match control {
4648        ControlFlow::Continue(_) => Err(KclError::refactor(format!("Source range not found: {source_range:?}"))),
4649        ControlFlow::Break(break_value) => break_value,
4650    }
4651}
4652
4653#[derive(Debug)]
4654struct AstMutateContext {
4655    source_range: SourceRange,
4656    node_path: Option<ast::NodePath>,
4657    command: AstMutateCommand,
4658    defined_names_stack: Vec<HashSet<String>>,
4659}
4660
4661#[derive(Debug)]
4662#[allow(clippy::large_enum_variant)]
4663enum AstMutateCommand {
4664    /// Add an expression statement to the sketch block.
4665    AddSketchBlockExprStmt {
4666        expr: ast::Expr,
4667    },
4668    /// Add a variable declaration to the sketch block (e.g. `line1 = line(...)`).
4669    AddSketchBlockVarDecl {
4670        prefix: String,
4671        expr: ast::Expr,
4672    },
4673    AddVariableDeclaration {
4674        prefix: String,
4675    },
4676    EditPoint {
4677        at: ast::Expr,
4678    },
4679    EditLine {
4680        start: ast::Expr,
4681        end: ast::Expr,
4682        construction: Option<bool>,
4683    },
4684    EditArc {
4685        start: ast::Expr,
4686        end: ast::Expr,
4687        center: ast::Expr,
4688        construction: Option<bool>,
4689    },
4690    EditCircle {
4691        start: ast::Expr,
4692        center: ast::Expr,
4693        construction: Option<bool>,
4694    },
4695    EditConstraintValue {
4696        value: ast::BinaryPart,
4697    },
4698    EditCallUnlabeled {
4699        arg: ast::Expr,
4700    },
4701    #[cfg(feature = "artifact-graph")]
4702    EditVarInitialValue {
4703        value: Number,
4704    },
4705    DeleteNode,
4706}
4707
4708impl AstMutateCommand {
4709    fn needs_defined_names_stack(&self) -> bool {
4710        matches!(
4711            self,
4712            AstMutateCommand::AddSketchBlockVarDecl { .. } | AstMutateCommand::AddVariableDeclaration { .. }
4713        )
4714    }
4715}
4716
4717#[derive(Debug)]
4718enum AstMutateCommandReturn {
4719    None,
4720    Name(String),
4721}
4722
4723#[derive(Debug, Clone)]
4724struct AstNodeRef {
4725    range: SourceRange,
4726    node_path: Option<ast::NodePath>,
4727}
4728
4729impl<T> From<&ast::Node<T>> for AstNodeRef {
4730    fn from(value: &ast::Node<T>) -> Self {
4731        AstNodeRef {
4732            range: value.into(),
4733            node_path: value.node_path.clone(),
4734        }
4735    }
4736}
4737
4738impl From<&ast::BodyItem> for AstNodeRef {
4739    fn from(value: &ast::BodyItem) -> Self {
4740        match value {
4741            ast::BodyItem::ImportStatement(node) => AstNodeRef {
4742                range: node.into(),
4743                node_path: node.node_path.clone(),
4744            },
4745            ast::BodyItem::ExpressionStatement(node) => AstNodeRef {
4746                range: node.into(),
4747                node_path: node.node_path.clone(),
4748            },
4749            ast::BodyItem::VariableDeclaration(node) => AstNodeRef {
4750                range: node.into(),
4751                node_path: node.node_path.clone(),
4752            },
4753            ast::BodyItem::TypeDeclaration(node) => AstNodeRef {
4754                range: node.into(),
4755                node_path: node.node_path.clone(),
4756            },
4757            ast::BodyItem::ReturnStatement(node) => AstNodeRef {
4758                range: node.into(),
4759                node_path: node.node_path.clone(),
4760            },
4761        }
4762    }
4763}
4764
4765impl From<&ast::Expr> for AstNodeRef {
4766    fn from(value: &ast::Expr) -> Self {
4767        AstNodeRef {
4768            range: SourceRange::from(value),
4769            node_path: value.node_path().cloned(),
4770        }
4771    }
4772}
4773
4774impl From<&AstMutateContext> for AstNodeRef {
4775    fn from(value: &AstMutateContext) -> Self {
4776        AstNodeRef {
4777            range: value.source_range,
4778            node_path: value.node_path.clone(),
4779        }
4780    }
4781}
4782
4783impl TryFrom<&NodeMut<'_>> for AstNodeRef {
4784    type Error = crate::walk::AstNodeError;
4785
4786    fn try_from(value: &NodeMut<'_>) -> Result<Self, Self::Error> {
4787        Ok(AstNodeRef {
4788            range: SourceRange::try_from(value)?,
4789            node_path: value.try_into()?,
4790        })
4791    }
4792}
4793
4794impl From<AstNodeRef> for SourceRange {
4795    fn from(value: AstNodeRef) -> Self {
4796        value.range
4797    }
4798}
4799
4800impl Visitor for AstMutateContext {
4801    type Break = Result<(AstNodeRef, AstMutateCommandReturn), KclError>;
4802    type Continue = ();
4803
4804    fn visit(&mut self, node: NodeMut<'_>) -> TraversalReturn<Self::Break, Self::Continue> {
4805        filter_and_process(self, node)
4806    }
4807
4808    fn finish(&mut self, node: NodeMut<'_>) {
4809        match &node {
4810            NodeMut::Program(_) | NodeMut::SketchBlock(_) => {
4811                self.defined_names_stack.pop();
4812            }
4813            _ => {}
4814        }
4815    }
4816}
4817
4818fn filter_and_process(
4819    ctx: &mut AstMutateContext,
4820    node: NodeMut,
4821) -> TraversalReturn<Result<(AstNodeRef, AstMutateCommandReturn), KclError>> {
4822    let Ok(node_range) = SourceRange::try_from(&node) else {
4823        // Nodes that can't be converted to a range aren't interesting.
4824        return TraversalReturn::new_continue(());
4825    };
4826    // If we're adding a variable declaration, we need to look at variable
4827    // declaration expressions to see if it already has a variable, before
4828    // continuing. The variable declaration's source range won't match the
4829    // target; its init expression will.
4830    if let NodeMut::VariableDeclaration(var_decl) = &node {
4831        let expr_range = SourceRange::from(&var_decl.declaration.init);
4832        if expr_range == ctx.source_range {
4833            if let AstMutateCommand::AddVariableDeclaration { .. } = &ctx.command {
4834                // We found the variable declaration expression. It doesn't need
4835                // to be added.
4836                return TraversalReturn::new_break(Ok((
4837                    AstNodeRef::from(&**var_decl),
4838                    AstMutateCommandReturn::Name(var_decl.name().to_owned()),
4839                )));
4840            }
4841            if let AstMutateCommand::DeleteNode = &ctx.command {
4842                // We found the variable declaration. Delete the variable along
4843                // with the segment.
4844                return TraversalReturn {
4845                    mutate_body_item: MutateBodyItem::Delete,
4846                    control_flow: ControlFlow::Break(Ok((AstNodeRef::from(&*ctx), AstMutateCommandReturn::None))),
4847                };
4848            }
4849        }
4850    }
4851
4852    if ctx.command.needs_defined_names_stack() {
4853        if let NodeMut::Program(program) = &node {
4854            ctx.defined_names_stack.push(find_defined_names(*program));
4855        } else if let NodeMut::SketchBlock(block) = &node {
4856            ctx.defined_names_stack.push(find_defined_names(&block.body));
4857        }
4858    }
4859
4860    // Make sure the node matches the source range.
4861    // TODO: Should we also check the NodePath?
4862    if node_range != ctx.source_range {
4863        return TraversalReturn::new_continue(());
4864    }
4865    let Ok(node_ref) = AstNodeRef::try_from(&node) else {
4866        return TraversalReturn::new_continue(());
4867    };
4868    process(ctx, node).map_break(|result| result.map(|cmd_return| (node_ref, cmd_return)))
4869}
4870
4871fn process(ctx: &AstMutateContext, node: NodeMut) -> TraversalReturn<Result<AstMutateCommandReturn, KclError>> {
4872    match &ctx.command {
4873        AstMutateCommand::AddSketchBlockExprStmt { expr } => {
4874            if let NodeMut::SketchBlock(sketch_block) = node {
4875                sketch_block
4876                    .body
4877                    .items
4878                    .push(ast::BodyItem::ExpressionStatement(ast::Node {
4879                        inner: ast::ExpressionStatement {
4880                            expression: expr.clone(),
4881                            digest: None,
4882                        },
4883                        start: Default::default(),
4884                        end: Default::default(),
4885                        module_id: Default::default(),
4886                        node_path: None,
4887                        outer_attrs: Default::default(),
4888                        pre_comments: Default::default(),
4889                        comment_start: Default::default(),
4890                    }));
4891                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
4892            }
4893        }
4894        AstMutateCommand::AddSketchBlockVarDecl { prefix, expr } => {
4895            if let NodeMut::SketchBlock(sketch_block) = node {
4896                let empty_defined_names = HashSet::new();
4897                let defined_names = ctx.defined_names_stack.last().unwrap_or(&empty_defined_names);
4898                let Ok(name) = next_free_name(prefix, defined_names) else {
4899                    return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
4900                };
4901                sketch_block
4902                    .body
4903                    .items
4904                    .push(ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(
4905                        ast::VariableDeclaration::new(
4906                            ast::VariableDeclarator::new(&name, expr.clone()),
4907                            ast::ItemVisibility::Default,
4908                            ast::VariableKind::Const,
4909                        ),
4910                    ))));
4911                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::Name(name)));
4912            }
4913        }
4914        AstMutateCommand::AddVariableDeclaration { prefix } => {
4915            if let NodeMut::VariableDeclaration(inner) = node {
4916                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::Name(inner.name().to_owned())));
4917            }
4918            if let NodeMut::ExpressionStatement(expr_stmt) = node {
4919                let empty_defined_names = HashSet::new();
4920                let defined_names = ctx.defined_names_stack.last().unwrap_or(&empty_defined_names);
4921                let Ok(name) = next_free_name(prefix, defined_names) else {
4922                    // TODO: Return an error instead?
4923                    return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
4924                };
4925                let mutate_node =
4926                    ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(ast::VariableDeclaration::new(
4927                        ast::VariableDeclarator::new(&name, expr_stmt.expression.clone()),
4928                        ast::ItemVisibility::Default,
4929                        ast::VariableKind::Const,
4930                    ))));
4931                return TraversalReturn {
4932                    mutate_body_item: MutateBodyItem::Mutate(Box::new(mutate_node)),
4933                    control_flow: ControlFlow::Break(Ok(AstMutateCommandReturn::Name(name))),
4934                };
4935            }
4936        }
4937        AstMutateCommand::EditPoint { at } => {
4938            if let NodeMut::CallExpressionKw(call) = node {
4939                if call.callee.name.name != POINT_FN {
4940                    return TraversalReturn::new_continue(());
4941                }
4942                // Update the arguments.
4943                for labeled_arg in &mut call.arguments {
4944                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(POINT_AT_PARAM) {
4945                        labeled_arg.arg = at.clone();
4946                    }
4947                }
4948                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
4949            }
4950        }
4951        AstMutateCommand::EditLine {
4952            start,
4953            end,
4954            construction,
4955        } => {
4956            if let NodeMut::CallExpressionKw(call) = node {
4957                if call.callee.name.name != LINE_FN {
4958                    return TraversalReturn::new_continue(());
4959                }
4960                // Update the arguments.
4961                for labeled_arg in &mut call.arguments {
4962                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(LINE_START_PARAM) {
4963                        labeled_arg.arg = start.clone();
4964                    }
4965                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(LINE_END_PARAM) {
4966                        labeled_arg.arg = end.clone();
4967                    }
4968                }
4969                // Handle construction kwarg
4970                if let Some(construction_value) = construction {
4971                    let construction_exists = call
4972                        .arguments
4973                        .iter()
4974                        .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
4975                    if *construction_value {
4976                        // Add or update construction=true
4977                        if construction_exists {
4978                            // Update existing construction kwarg
4979                            for labeled_arg in &mut call.arguments {
4980                                if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
4981                                    labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
4982                                        value: ast::LiteralValue::Bool(true),
4983                                        raw: "true".to_string(),
4984                                        digest: None,
4985                                    })));
4986                                }
4987                            }
4988                        } else {
4989                            // Add new construction kwarg
4990                            call.arguments.push(ast::LabeledArg {
4991                                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
4992                                arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
4993                                    value: ast::LiteralValue::Bool(true),
4994                                    raw: "true".to_string(),
4995                                    digest: None,
4996                                }))),
4997                            });
4998                        }
4999                    } else {
5000                        // Remove construction kwarg if it exists
5001                        call.arguments
5002                            .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
5003                    }
5004                }
5005                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5006            }
5007        }
5008        AstMutateCommand::EditArc {
5009            start,
5010            end,
5011            center,
5012            construction,
5013        } => {
5014            if let NodeMut::CallExpressionKw(call) = node {
5015                if call.callee.name.name != ARC_FN {
5016                    return TraversalReturn::new_continue(());
5017                }
5018                // Update the arguments.
5019                for labeled_arg in &mut call.arguments {
5020                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_START_PARAM) {
5021                        labeled_arg.arg = start.clone();
5022                    }
5023                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_END_PARAM) {
5024                        labeled_arg.arg = end.clone();
5025                    }
5026                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_CENTER_PARAM) {
5027                        labeled_arg.arg = center.clone();
5028                    }
5029                }
5030                // Handle construction kwarg
5031                if let Some(construction_value) = construction {
5032                    let construction_exists = call
5033                        .arguments
5034                        .iter()
5035                        .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
5036                    if *construction_value {
5037                        // Add or update construction=true
5038                        if construction_exists {
5039                            // Update existing construction kwarg
5040                            for labeled_arg in &mut call.arguments {
5041                                if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
5042                                    labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
5043                                        value: ast::LiteralValue::Bool(true),
5044                                        raw: "true".to_string(),
5045                                        digest: None,
5046                                    })));
5047                                }
5048                            }
5049                        } else {
5050                            // Add new construction kwarg
5051                            call.arguments.push(ast::LabeledArg {
5052                                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
5053                                arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
5054                                    value: ast::LiteralValue::Bool(true),
5055                                    raw: "true".to_string(),
5056                                    digest: None,
5057                                }))),
5058                            });
5059                        }
5060                    } else {
5061                        // Remove construction kwarg if it exists
5062                        call.arguments
5063                            .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
5064                    }
5065                }
5066                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5067            }
5068        }
5069        AstMutateCommand::EditCircle {
5070            start,
5071            center,
5072            construction,
5073        } => {
5074            if let NodeMut::CallExpressionKw(call) = node {
5075                if call.callee.name.name != CIRCLE_FN {
5076                    return TraversalReturn::new_continue(());
5077                }
5078                // Update the arguments.
5079                for labeled_arg in &mut call.arguments {
5080                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CIRCLE_START_PARAM) {
5081                        labeled_arg.arg = start.clone();
5082                    }
5083                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CIRCLE_CENTER_PARAM) {
5084                        labeled_arg.arg = center.clone();
5085                    }
5086                }
5087                // Handle construction kwarg
5088                if let Some(construction_value) = construction {
5089                    let construction_exists = call
5090                        .arguments
5091                        .iter()
5092                        .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
5093                    if *construction_value {
5094                        if construction_exists {
5095                            for labeled_arg in &mut call.arguments {
5096                                if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
5097                                    labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
5098                                        value: ast::LiteralValue::Bool(true),
5099                                        raw: "true".to_string(),
5100                                        digest: None,
5101                                    })));
5102                                }
5103                            }
5104                        } else {
5105                            call.arguments.push(ast::LabeledArg {
5106                                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
5107                                arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
5108                                    value: ast::LiteralValue::Bool(true),
5109                                    raw: "true".to_string(),
5110                                    digest: None,
5111                                }))),
5112                            });
5113                        }
5114                    } else {
5115                        call.arguments
5116                            .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
5117                    }
5118                }
5119                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5120            }
5121        }
5122        AstMutateCommand::EditConstraintValue { value } => {
5123            if let NodeMut::BinaryExpression(binary_expr) = node {
5124                let left_is_constraint = matches!(
5125                    &binary_expr.left,
5126                    ast::BinaryPart::CallExpressionKw(call)
5127                        if matches!(
5128                            call.callee.name.name.as_str(),
5129                            DISTANCE_FN | HORIZONTAL_DISTANCE_FN | VERTICAL_DISTANCE_FN | RADIUS_FN | DIAMETER_FN | ANGLE_FN
5130                        )
5131                );
5132                if left_is_constraint {
5133                    binary_expr.right = value.clone();
5134                } else {
5135                    binary_expr.left = value.clone();
5136                }
5137
5138                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5139            }
5140        }
5141        AstMutateCommand::EditCallUnlabeled { arg } => {
5142            if let NodeMut::CallExpressionKw(call) = node {
5143                call.unlabeled = Some(arg.clone());
5144                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5145            }
5146        }
5147        #[cfg(feature = "artifact-graph")]
5148        AstMutateCommand::EditVarInitialValue { value } => {
5149            if let NodeMut::NumericLiteral(numeric_literal) = node {
5150                // Update the initial value.
5151                let Ok(literal) = to_source_number(*value) else {
5152                    return TraversalReturn::new_break(Err(KclError::refactor(format!(
5153                        "Could not convert number to AST literal: {:?}",
5154                        *value
5155                    ))));
5156                };
5157                *numeric_literal = ast::Node::no_src(literal);
5158                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5159            }
5160        }
5161        AstMutateCommand::DeleteNode => {
5162            return TraversalReturn {
5163                mutate_body_item: MutateBodyItem::Delete,
5164                control_flow: ControlFlow::Break(Ok(AstMutateCommandReturn::None)),
5165            };
5166        }
5167    }
5168    TraversalReturn::new_continue(())
5169}
5170
5171struct FindSketchBlockSourceRange {
5172    /// The source range of the sketch block before mutation.
5173    target_before_mutation: SourceRange,
5174    /// The source range of the sketch block's last body item after mutation. We
5175    /// need to use a [Cell] since the [crate::walk::Visitor] trait requires a
5176    /// shared reference.
5177    found: Cell<Option<AstNodeRef>>,
5178}
5179
5180impl<'a> crate::walk::Visitor<'a> for &FindSketchBlockSourceRange {
5181    type Error = crate::front::Error;
5182
5183    fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
5184        let Ok(node_range) = SourceRange::try_from(&node) else {
5185            return Ok(true);
5186        };
5187
5188        if let crate::walk::Node::SketchBlock(sketch_block) = node {
5189            if node_range.module_id() == self.target_before_mutation.module_id()
5190                && node_range.start() == self.target_before_mutation.start()
5191                // End shouldn't match since we added something.
5192                && node_range.end() >= self.target_before_mutation.end()
5193            {
5194                self.found.set(sketch_block.body.items.last().map(|item| match item {
5195                    // For declarations like `circle1 = circle(...)`, use
5196                    // the init expression range so lookup in source_range_to_object
5197                    // matches the segment source range.
5198                    ast::BodyItem::VariableDeclaration(node) => AstNodeRef::from(&node.declaration.init),
5199                    _ => AstNodeRef::from(item),
5200                }));
5201                return Ok(false);
5202            } else {
5203                // We found a different sketch block. No need to descend into
5204                // its children since sketch blocks cannot be nested.
5205                return Ok(true);
5206            }
5207        }
5208
5209        for child in node.children().iter() {
5210            if !child.visit(*self)? {
5211                return Ok(false);
5212            }
5213        }
5214
5215        Ok(true)
5216    }
5217}
5218
5219struct FindSketchBlockByNodePath {
5220    /// The Node Path of the sketch block before mutation.
5221    target_node_path: ast::NodePath,
5222    /// The ref of the sketch block's last body item after mutation. We need to
5223    /// use a [Cell] since the [crate::walk::Visitor] trait requires a shared
5224    /// reference.
5225    found: Cell<Option<AstNodeRef>>,
5226}
5227
5228impl<'a> crate::walk::Visitor<'a> for &FindSketchBlockByNodePath {
5229    type Error = crate::front::Error;
5230
5231    fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
5232        let Ok(node_path) = <Option<ast::NodePath>>::try_from(&node) else {
5233            return Ok(true);
5234        };
5235
5236        if let crate::walk::Node::SketchBlock(sketch_block) = node {
5237            if let Some(node_path) = node_path
5238                && node_path == self.target_node_path
5239            {
5240                self.found.set(sketch_block.body.items.last().map(|item| match item {
5241                    // For declarations like `circle1 = circle(...)`, use
5242                    // the init expression range so lookup in source_range_to_object
5243                    // matches the segment source range.
5244                    ast::BodyItem::VariableDeclaration(node) => AstNodeRef::from(&node.declaration.init),
5245                    _ => AstNodeRef::from(item),
5246                }));
5247
5248                return Ok(false);
5249            } else {
5250                // We found a different sketch block. No need to descend into
5251                // its children since sketch blocks cannot be nested.
5252                return Ok(true);
5253            }
5254        }
5255
5256        for child in node.children().iter() {
5257            if !child.visit(*self)? {
5258                return Ok(false);
5259            }
5260        }
5261
5262        Ok(true)
5263    }
5264}
5265
5266/// After adding an item to a sketch block, find the sketch block, and get the
5267/// source range of the added item. We assume that the added item is the last
5268/// item in the sketch block and that the sketch block's source range has grown,
5269/// but not moved from its starting offset.
5270///
5271/// TODO: Do we need to format *before* mutation in case formatting moves the
5272/// sketch block forward?
5273fn find_sketch_block_added_item(
5274    ast: &ast::Node<ast::Program>,
5275    sketch_block_before_mutation: &AstNodeRef,
5276) -> Result<AstNodeRef, KclError> {
5277    if let Some(node_path) = &sketch_block_before_mutation.node_path {
5278        let find = FindSketchBlockByNodePath {
5279            target_node_path: node_path.clone(),
5280            found: Cell::new(None),
5281        };
5282        let node = crate::walk::Node::from(ast);
5283        node.visit(&find).map_err(|err| KclError::refactor(err.msg))?;
5284        find.found.into_inner().ok_or_else(|| {
5285            KclError::refactor(format!(
5286                "Node ID after mutation not found for Node ID before mutation: {node_path:?}"
5287            ))
5288        })
5289    } else {
5290        // No NodePath. Fall back to legacy source range.
5291        let find = FindSketchBlockSourceRange {
5292            target_before_mutation: sketch_block_before_mutation.range,
5293            found: Cell::new(None),
5294        };
5295        let node = crate::walk::Node::from(ast);
5296        node.visit(&find).map_err(|err| KclError::refactor(err.msg))?;
5297        find.found.into_inner().ok_or_else(|| KclError::refactor(
5298            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?"),
5299        ))
5300    }
5301}
5302
5303fn source_from_ast(ast: &ast::Node<ast::Program>) -> String {
5304    // TODO: Don't duplicate this from lib.rs Program.
5305    ast.recast_top(&Default::default(), 0)
5306}
5307
5308pub(crate) fn to_ast_point2d(point: &Point2d<Expr>) -> anyhow::Result<ast::Expr> {
5309    Ok(ast::Expr::ArrayExpression(Box::new(ast::Node {
5310        inner: ast::ArrayExpression {
5311            elements: vec![to_source_expr(&point.x)?, to_source_expr(&point.y)?],
5312            non_code_meta: Default::default(),
5313            digest: None,
5314        },
5315        start: Default::default(),
5316        end: Default::default(),
5317        module_id: Default::default(),
5318        node_path: None,
5319        outer_attrs: Default::default(),
5320        pre_comments: Default::default(),
5321        comment_start: Default::default(),
5322    })))
5323}
5324
5325fn to_source_expr(expr: &Expr) -> anyhow::Result<ast::Expr> {
5326    match expr {
5327        Expr::Number(number) => Ok(ast::Expr::Literal(Box::new(ast::Node {
5328            inner: ast::Literal::from(to_source_number(*number)?),
5329            start: Default::default(),
5330            end: Default::default(),
5331            module_id: Default::default(),
5332            node_path: None,
5333            outer_attrs: Default::default(),
5334            pre_comments: Default::default(),
5335            comment_start: Default::default(),
5336        }))),
5337        Expr::Var(number) => Ok(ast::Expr::SketchVar(Box::new(ast::Node {
5338            inner: ast::SketchVar {
5339                initial: Some(Box::new(ast::Node {
5340                    inner: to_source_number(*number)?,
5341                    start: Default::default(),
5342                    end: Default::default(),
5343                    module_id: Default::default(),
5344                    node_path: None,
5345                    outer_attrs: Default::default(),
5346                    pre_comments: Default::default(),
5347                    comment_start: Default::default(),
5348                })),
5349                digest: None,
5350            },
5351            start: Default::default(),
5352            end: Default::default(),
5353            module_id: Default::default(),
5354            node_path: None,
5355            outer_attrs: Default::default(),
5356            pre_comments: Default::default(),
5357            comment_start: Default::default(),
5358        }))),
5359        Expr::Variable(variable) => Ok(ast_name_expr(variable.clone())),
5360    }
5361}
5362
5363fn to_source_number(number: Number) -> anyhow::Result<ast::NumericLiteral> {
5364    Ok(ast::NumericLiteral {
5365        value: number.value,
5366        suffix: number.units,
5367        raw: format_number_literal(number.value, number.units, None)?,
5368        digest: None,
5369    })
5370}
5371
5372pub(crate) fn ast_name_expr(name: String) -> ast::Expr {
5373    ast::Expr::Name(Box::new(ast_name(name)))
5374}
5375
5376fn ast_name(name: String) -> ast::Node<ast::Name> {
5377    ast::Node {
5378        inner: ast::Name {
5379            name: ast::Node {
5380                inner: ast::Identifier { name, digest: None },
5381                start: Default::default(),
5382                end: Default::default(),
5383                module_id: Default::default(),
5384                node_path: None,
5385                outer_attrs: Default::default(),
5386                pre_comments: Default::default(),
5387                comment_start: Default::default(),
5388            },
5389            path: Vec::new(),
5390            abs_path: false,
5391            digest: None,
5392        },
5393        start: Default::default(),
5394        end: Default::default(),
5395        module_id: Default::default(),
5396        node_path: None,
5397        outer_attrs: Default::default(),
5398        pre_comments: Default::default(),
5399        comment_start: Default::default(),
5400    }
5401}
5402
5403pub(crate) fn ast_sketch2_name(name: &str) -> ast::Name {
5404    ast::Name {
5405        name: ast::Node {
5406            inner: ast::Identifier {
5407                name: name.to_owned(),
5408                digest: None,
5409            },
5410            start: Default::default(),
5411            end: Default::default(),
5412            module_id: Default::default(),
5413            node_path: None,
5414            outer_attrs: Default::default(),
5415            pre_comments: Default::default(),
5416            comment_start: Default::default(),
5417        },
5418        path: Default::default(),
5419        abs_path: false,
5420        digest: None,
5421    }
5422}
5423
5424// Shared AST creation helpers used by both frontend and transpiler to ensure consistency.
5425
5426/// Create an AST node for coincident([expr1, expr2, ...])
5427pub(crate) fn create_coincident_ast(exprs: impl IntoIterator<Item = ast::Expr>) -> ast::Expr {
5428    let elements = exprs.into_iter().collect::<Vec<_>>();
5429    debug_assert!(elements.len() >= 2, "Coincident AST should have at least 2 inputs");
5430
5431    // Create array [expr1, expr2, ...]
5432    let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
5433        elements,
5434        digest: None,
5435        non_code_meta: Default::default(),
5436    })));
5437
5438    // Create coincident([...])
5439    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5440        callee: ast::Node::no_src(ast_sketch2_name(COINCIDENT_FN)),
5441        unlabeled: Some(array_expr),
5442        arguments: Default::default(),
5443        digest: None,
5444        non_code_meta: Default::default(),
5445    })))
5446}
5447
5448/// Create an AST node for line(start = [...], end = [...])
5449pub(crate) fn create_line_ast(start_ast: ast::Expr, end_ast: ast::Expr) -> ast::Expr {
5450    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5451        callee: ast::Node::no_src(ast_sketch2_name(LINE_FN)),
5452        unlabeled: None,
5453        arguments: vec![
5454            ast::LabeledArg {
5455                label: Some(ast::Identifier::new(LINE_START_PARAM)),
5456                arg: start_ast,
5457            },
5458            ast::LabeledArg {
5459                label: Some(ast::Identifier::new(LINE_END_PARAM)),
5460                arg: end_ast,
5461            },
5462        ],
5463        digest: None,
5464        non_code_meta: Default::default(),
5465    })))
5466}
5467
5468/// Create an AST node for arc(start = [...], end = [...], center = [...])
5469pub(crate) fn create_arc_ast(start_ast: ast::Expr, end_ast: ast::Expr, center_ast: ast::Expr) -> ast::Expr {
5470    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5471        callee: ast::Node::no_src(ast_sketch2_name(ARC_FN)),
5472        unlabeled: None,
5473        arguments: vec![
5474            ast::LabeledArg {
5475                label: Some(ast::Identifier::new(ARC_START_PARAM)),
5476                arg: start_ast,
5477            },
5478            ast::LabeledArg {
5479                label: Some(ast::Identifier::new(ARC_END_PARAM)),
5480                arg: end_ast,
5481            },
5482            ast::LabeledArg {
5483                label: Some(ast::Identifier::new(ARC_CENTER_PARAM)),
5484                arg: center_ast,
5485            },
5486        ],
5487        digest: None,
5488        non_code_meta: Default::default(),
5489    })))
5490}
5491
5492/// Create an AST node for circle(start = [...], center = [...])
5493pub(crate) fn create_circle_ast(start_ast: ast::Expr, center_ast: ast::Expr) -> ast::Expr {
5494    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5495        callee: ast::Node::no_src(ast_sketch2_name(CIRCLE_FN)),
5496        unlabeled: None,
5497        arguments: vec![
5498            ast::LabeledArg {
5499                label: Some(ast::Identifier::new(CIRCLE_START_PARAM)),
5500                arg: start_ast,
5501            },
5502            ast::LabeledArg {
5503                label: Some(ast::Identifier::new(CIRCLE_CENTER_PARAM)),
5504                arg: center_ast,
5505            },
5506        ],
5507        digest: None,
5508        non_code_meta: Default::default(),
5509    })))
5510}
5511
5512/// Create an AST node for horizontal(line)
5513pub(crate) fn create_horizontal_ast(line_expr: ast::Expr) -> ast::Expr {
5514    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5515        callee: ast::Node::no_src(ast_sketch2_name(HORIZONTAL_FN)),
5516        unlabeled: Some(line_expr),
5517        arguments: Default::default(),
5518        digest: None,
5519        non_code_meta: Default::default(),
5520    })))
5521}
5522
5523/// Create an AST node for vertical(line)
5524pub(crate) fn create_vertical_ast(line_expr: ast::Expr) -> ast::Expr {
5525    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5526        callee: ast::Node::no_src(ast_sketch2_name(VERTICAL_FN)),
5527        unlabeled: Some(line_expr),
5528        arguments: Default::default(),
5529        digest: None,
5530        non_code_meta: Default::default(),
5531    })))
5532}
5533
5534/// Create a member expression like object.property (e.g., line1.end)
5535pub(crate) fn create_member_expression(object_expr: ast::Expr, property: &str) -> ast::Expr {
5536    ast::Expr::MemberExpression(Box::new(ast::Node::no_src(ast::MemberExpression {
5537        object: object_expr,
5538        property: ast::Expr::Name(Box::new(ast::Node::no_src(ast::Name {
5539            name: ast::Node::no_src(ast::Identifier {
5540                name: property.to_string(),
5541                digest: None,
5542            }),
5543            path: Vec::new(),
5544            abs_path: false,
5545            digest: None,
5546        }))),
5547        computed: false,
5548        digest: None,
5549    })))
5550}
5551
5552/// Create an AST node for `fixed([point, [x, y]])`.
5553fn create_fixed_point_constraint_ast(point_expr: ast::Expr, position: Point2d<Number>) -> anyhow::Result<ast::Expr> {
5554    // Create [x, y] array literal.
5555    let x_literal = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(to_source_number(
5556        position.x,
5557    )?))));
5558    let y_literal = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(to_source_number(
5559        position.y,
5560    )?))));
5561    let point_array = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
5562        elements: vec![x_literal, y_literal],
5563        digest: None,
5564        non_code_meta: Default::default(),
5565    })));
5566
5567    // Create [point, [x, y]] outer array.
5568    let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
5569        elements: vec![point_expr, point_array],
5570        digest: None,
5571        non_code_meta: Default::default(),
5572    })));
5573
5574    // Create fixed([...])
5575    Ok(ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(
5576        ast::CallExpressionKw {
5577            callee: ast::Node::no_src(ast_sketch2_name(FIXED_FN)),
5578            unlabeled: Some(array_expr),
5579            arguments: Default::default(),
5580            digest: None,
5581            non_code_meta: Default::default(),
5582        },
5583    ))))
5584}
5585
5586/// Create an AST node for equalLength([line1, line2, ...])
5587pub(crate) fn create_equal_length_ast(line_exprs: Vec<ast::Expr>) -> ast::Expr {
5588    let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
5589        elements: line_exprs,
5590        digest: None,
5591        non_code_meta: Default::default(),
5592    })));
5593
5594    // Create equalLength([...])
5595    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5596        callee: ast::Node::no_src(ast_sketch2_name(EQUAL_LENGTH_FN)),
5597        unlabeled: Some(array_expr),
5598        arguments: Default::default(),
5599        digest: None,
5600        non_code_meta: Default::default(),
5601    })))
5602}
5603
5604/// Create an AST node for equalRadius([seg1, seg2, ...])
5605pub(crate) fn create_equal_radius_ast(segment_exprs: Vec<ast::Expr>) -> ast::Expr {
5606    let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
5607        elements: segment_exprs,
5608        digest: None,
5609        non_code_meta: Default::default(),
5610    })));
5611
5612    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5613        callee: ast::Node::no_src(ast_sketch2_name(EQUAL_RADIUS_FN)),
5614        unlabeled: Some(array_expr),
5615        arguments: Default::default(),
5616        digest: None,
5617        non_code_meta: Default::default(),
5618    })))
5619}
5620
5621/// Create an AST node for tangent([seg1, seg2])
5622pub(crate) fn create_tangent_ast(seg1_expr: ast::Expr, seg2_expr: ast::Expr) -> ast::Expr {
5623    let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
5624        elements: vec![seg1_expr, seg2_expr],
5625        digest: None,
5626        non_code_meta: Default::default(),
5627    })));
5628
5629    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5630        callee: ast::Node::no_src(ast_sketch2_name(TANGENT_FN)),
5631        unlabeled: Some(array_expr),
5632        arguments: Default::default(),
5633        digest: None,
5634        non_code_meta: Default::default(),
5635    })))
5636}
5637
5638/// Create an AST node for midpoint(segment, point = point)
5639pub(crate) fn create_midpoint_ast(segment_expr: ast::Expr, point_expr: ast::Expr) -> ast::Expr {
5640    let arguments = vec![ast::LabeledArg {
5641        label: Some(ast::Identifier::new(MIDPOINT_POINT_PARAM)),
5642        arg: point_expr,
5643    }];
5644
5645    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5646        callee: ast::Node::no_src(ast_sketch2_name(MIDPOINT_FN)),
5647        unlabeled: Some(segment_expr),
5648        arguments,
5649        digest: None,
5650        non_code_meta: Default::default(),
5651    })))
5652}
5653
5654#[cfg(all(feature = "artifact-graph", test))]
5655mod tests {
5656    use super::*;
5657    use crate::engine::PlaneName;
5658    use crate::execution::cache::SketchModeState;
5659    use crate::execution::cache::clear_mem_cache;
5660    use crate::execution::cache::read_old_memory;
5661    use crate::execution::cache::write_old_memory;
5662    use crate::front::Distance;
5663    use crate::front::Fixed;
5664    use crate::front::FixedPoint;
5665    use crate::front::Midpoint;
5666    use crate::front::Object;
5667    use crate::front::Plane;
5668    use crate::front::Sketch;
5669    use crate::front::Tangent;
5670    use crate::frontend::sketch::Vertical;
5671    use crate::pretty::NumericSuffix;
5672
5673    fn find_first_sketch_object(scene_graph: &SceneGraph) -> Option<&Object> {
5674        for object in &scene_graph.objects {
5675            if let ObjectKind::Sketch(_) = &object.kind {
5676                return Some(object);
5677            }
5678        }
5679        None
5680    }
5681
5682    fn find_first_face_object(scene_graph: &SceneGraph) -> Option<&Object> {
5683        for object in &scene_graph.objects {
5684            if let ObjectKind::Face(_) = &object.kind {
5685                return Some(object);
5686            }
5687        }
5688        None
5689    }
5690
5691    fn find_first_wall_object_id(scene_graph: &SceneGraph) -> Option<ObjectId> {
5692        for object in &scene_graph.objects {
5693            if matches!(&object.kind, ObjectKind::Wall(_)) {
5694                return Some(object.id);
5695            }
5696        }
5697        None
5698    }
5699
5700    #[test]
5701    fn test_region_name_from_sweep_variable_supports_sweep_kinds() {
5702        let source = "\
5703region001 = region(point = [0.1, 0.1], sketch = s)
5704extrude001 = extrude(region001, length = 5)
5705revolve001 = revolve(region001, axis = Y)
5706sweep001 = sweep(region001, path = path001)
5707loft001 = loft(region001)
5708not_sweep001 = shell(extrude001, faces = [], thickness = 1)
5709";
5710
5711        let program = Program::parse(source).unwrap().0.unwrap();
5712
5713        assert_eq!(
5714            region_name_from_sweep_variable(&program.ast, "extrude001"),
5715            Some("region001".to_owned())
5716        );
5717        assert_eq!(
5718            region_name_from_sweep_variable(&program.ast, "revolve001"),
5719            Some("region001".to_owned())
5720        );
5721        assert_eq!(
5722            region_name_from_sweep_variable(&program.ast, "sweep001"),
5723            Some("region001".to_owned())
5724        );
5725        assert_eq!(
5726            region_name_from_sweep_variable(&program.ast, "loft001"),
5727            Some("region001".to_owned())
5728        );
5729        assert_eq!(region_name_from_sweep_variable(&program.ast, "not_sweep001"), None);
5730    }
5731
5732    #[track_caller]
5733    fn expect_sketch(object: &Object) -> &Sketch {
5734        if let ObjectKind::Sketch(sketch) = &object.kind {
5735            sketch
5736        } else {
5737            panic!("Object is not a sketch: {:?}", object);
5738        }
5739    }
5740
5741    fn make_line_ctor(start_x: f64, start_y: f64, end_x: f64, end_y: f64, units: NumericSuffix) -> LineCtor {
5742        LineCtor {
5743            start: Point2d {
5744                x: Expr::Number(Number { value: start_x, units }),
5745                y: Expr::Number(Number { value: start_y, units }),
5746            },
5747            end: Point2d {
5748                x: Expr::Number(Number { value: end_x, units }),
5749                y: Expr::Number(Number { value: end_y, units }),
5750            },
5751            construction: None,
5752        }
5753    }
5754
5755    async fn create_sketch_with_single_line(
5756        frontend: &mut FrontendState,
5757        ctx: &ExecutorContext,
5758        mock_ctx: &ExecutorContext,
5759        version: Version,
5760    ) -> (ObjectId, ObjectId, SourceDelta, SceneGraphDelta) {
5761        frontend.program = Program::empty();
5762
5763        let sketch_args = SketchCtor {
5764            on: Plane::Default(PlaneName::Xy),
5765        };
5766        let (_src_delta, _scene_delta, sketch_id) = frontend
5767            .new_sketch(ctx, ProjectId(0), FileId(0), version, sketch_args)
5768            .await
5769            .unwrap();
5770
5771        let segment = SegmentCtor::Line(make_line_ctor(0.0, 0.0, 10.0, 10.0, NumericSuffix::Mm));
5772        let (source_delta, scene_graph_delta) = frontend
5773            .add_segment(mock_ctx, version, sketch_id, segment, None)
5774            .await
5775            .unwrap();
5776        let line_id = *scene_graph_delta
5777            .new_objects
5778            .last()
5779            .expect("Expected line object id to be created");
5780
5781        (sketch_id, line_id, source_delta, scene_graph_delta)
5782    }
5783
5784    #[tokio::test(flavor = "multi_thread")]
5785    async fn test_sketch_checkpoint_round_trip_restores_state() {
5786        let mut frontend = FrontendState::new();
5787        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
5788        let mock_ctx = ExecutorContext::new_mock(None).await;
5789        let version = Version(0);
5790
5791        let (sketch_id, line_id, source_delta, scene_graph_delta) =
5792            create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
5793
5794        let expected_source = source_delta.text.clone();
5795        let expected_scene_graph = frontend.scene_graph.clone();
5796        let expected_exec_outcome = scene_graph_delta.exec_outcome.clone();
5797        let expected_point_freedom_cache = frontend.point_freedom_cache.clone();
5798
5799        let checkpoint_id = frontend
5800            .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
5801            .await
5802            .unwrap();
5803
5804        let edited_segments = vec![ExistingSegmentCtor {
5805            id: line_id,
5806            ctor: SegmentCtor::Line(make_line_ctor(1.0, 2.0, 13.0, 14.0, NumericSuffix::Mm)),
5807        }];
5808        let (edited_source, _edited_scene) = frontend
5809            .edit_segments(&mock_ctx, version, sketch_id, edited_segments)
5810            .await
5811            .unwrap();
5812        assert_ne!(edited_source.text, expected_source);
5813
5814        let restored = frontend.restore_sketch_checkpoint(checkpoint_id).await.unwrap();
5815
5816        assert_eq!(restored.source_delta.text, expected_source);
5817        assert_eq!(restored.scene_graph_delta.new_graph, expected_scene_graph);
5818        assert!(restored.scene_graph_delta.invalidates_ids);
5819        assert_eq!(restored.scene_graph_delta.exec_outcome, expected_exec_outcome);
5820        assert_eq!(frontend.scene_graph, expected_scene_graph);
5821        assert_eq!(frontend.point_freedom_cache, expected_point_freedom_cache);
5822
5823        ctx.close().await;
5824        mock_ctx.close().await;
5825    }
5826
5827    #[tokio::test(flavor = "multi_thread")]
5828    async fn test_sketch_checkpoints_prune_oldest_entries() {
5829        let mut frontend = FrontendState::new();
5830        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
5831        let mock_ctx = ExecutorContext::new_mock(None).await;
5832        let version = Version(0);
5833
5834        let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
5835            create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
5836
5837        let mut checkpoint_ids = Vec::new();
5838        for _ in 0..(MAX_SKETCH_CHECKPOINTS + 3) {
5839            checkpoint_ids.push(
5840                frontend
5841                    .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
5842                    .await
5843                    .unwrap(),
5844            );
5845        }
5846
5847        assert_eq!(frontend.sketch_checkpoints.len(), MAX_SKETCH_CHECKPOINTS);
5848        assert!(checkpoint_ids.windows(2).all(|ids| ids[0] < ids[1]));
5849
5850        let oldest_retained = checkpoint_ids[3];
5851        assert_eq!(
5852            frontend.sketch_checkpoints.front().map(|checkpoint| checkpoint.id),
5853            Some(oldest_retained)
5854        );
5855
5856        let evicted_restore = frontend.restore_sketch_checkpoint(checkpoint_ids[0]).await;
5857        assert!(evicted_restore.is_err());
5858        assert!(evicted_restore.unwrap_err().msg.contains("Sketch checkpoint not found"));
5859
5860        frontend
5861            .restore_sketch_checkpoint(*checkpoint_ids.last().unwrap())
5862            .await
5863            .unwrap();
5864
5865        ctx.close().await;
5866        mock_ctx.close().await;
5867    }
5868
5869    #[tokio::test(flavor = "multi_thread")]
5870    async fn test_restore_sketch_checkpoint_missing_id_returns_error() {
5871        let mut frontend = FrontendState::new();
5872        let missing_checkpoint = SketchCheckpointId::new(999);
5873
5874        let err = frontend
5875            .restore_sketch_checkpoint(missing_checkpoint)
5876            .await
5877            .expect_err("Expected restore to fail for missing checkpoint");
5878
5879        assert!(err.msg.contains("Sketch checkpoint not found"));
5880    }
5881
5882    #[tokio::test(flavor = "multi_thread")]
5883    async fn test_clear_sketch_checkpoints_removes_all_restore_points() {
5884        let mut frontend = FrontendState::new();
5885        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
5886        let mock_ctx = ExecutorContext::new_mock(None).await;
5887        let version = Version(0);
5888
5889        let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
5890            create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
5891
5892        let checkpoint_a = frontend
5893            .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
5894            .await
5895            .unwrap();
5896        let checkpoint_b = frontend
5897            .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
5898            .await
5899            .unwrap();
5900        assert_eq!(frontend.sketch_checkpoints.len(), 2);
5901
5902        frontend.clear_sketch_checkpoints();
5903        assert!(frontend.sketch_checkpoints.is_empty());
5904        frontend.restore_sketch_checkpoint(checkpoint_a).await.unwrap_err();
5905        frontend.restore_sketch_checkpoint(checkpoint_b).await.unwrap_err();
5906
5907        ctx.close().await;
5908        mock_ctx.close().await;
5909    }
5910
5911    #[tokio::test(flavor = "multi_thread")]
5912    async fn test_hack_set_program_keeps_old_checkpoints_and_adds_fresh_baseline() {
5913        let mut frontend = FrontendState::new();
5914        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
5915        let mock_ctx = ExecutorContext::new_mock(None).await;
5916        let version = Version(0);
5917
5918        let (_sketch_id, _line_id, source_delta, scene_graph_delta) =
5919            create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
5920        let old_source = source_delta.text.clone();
5921        let old_checkpoint = frontend
5922            .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
5923            .await
5924            .unwrap();
5925        let initial_checkpoint_count = frontend.sketch_checkpoints.len();
5926
5927        let new_program = Program::parse("sketch(on = XY) {\n  point(at = [1mm, 2mm])\n}\n")
5928            .unwrap()
5929            .0
5930            .unwrap();
5931
5932        let result = frontend.hack_set_program(&ctx, new_program).await.unwrap();
5933        let SetProgramOutcome::Success {
5934            checkpoint_id: Some(new_checkpoint),
5935            ..
5936        } = result
5937        else {
5938            panic!("Expected Success with a fresh checkpoint baseline");
5939        };
5940
5941        assert_eq!(frontend.sketch_checkpoints.len(), initial_checkpoint_count + 1);
5942
5943        let old_restore = frontend.restore_sketch_checkpoint(old_checkpoint).await.unwrap();
5944        assert_eq!(old_restore.source_delta.text, old_source);
5945
5946        let new_restore = frontend.restore_sketch_checkpoint(new_checkpoint).await.unwrap();
5947        assert!(new_restore.source_delta.text.contains("point(at = [1mm, 2mm])"));
5948
5949        ctx.close().await;
5950        mock_ctx.close().await;
5951    }
5952
5953    #[tokio::test(flavor = "multi_thread")]
5954    async fn test_hack_set_program_exec_failure_does_not_add_checkpoint() {
5955        let mut frontend = FrontendState::new();
5956        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
5957        let mock_ctx = ExecutorContext::new_mock(None).await;
5958        let version = Version(0);
5959
5960        let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
5961            create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
5962        let old_checkpoint = frontend
5963            .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
5964            .await
5965            .unwrap();
5966        let checkpoint_count_before = frontend.sketch_checkpoints.len();
5967
5968        let failing_program = Program::parse(
5969            "sketch(on = XY) {\n  line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])\n}\n\nbad = missing_name\n",
5970        )
5971        .unwrap()
5972        .0
5973        .unwrap();
5974
5975        let result = frontend.hack_set_program(&ctx, failing_program).await.unwrap();
5976        assert!(matches!(result, SetProgramOutcome::ExecFailure { .. }));
5977        assert_eq!(frontend.sketch_checkpoints.len(), checkpoint_count_before);
5978        frontend.restore_sketch_checkpoint(old_checkpoint).await.unwrap();
5979
5980        ctx.close().await;
5981        mock_ctx.close().await;
5982    }
5983
5984    #[tokio::test(flavor = "multi_thread")]
5985    async fn test_restore_sketch_checkpoint_restores_and_clears_mock_memory() {
5986        let mut frontend = FrontendState::new();
5987        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
5988
5989        let program = Program::parse(
5990            "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",
5991        )
5992        .unwrap()
5993        .0
5994        .unwrap();
5995        let set_program_outcome = frontend.hack_set_program(&ctx, program).await.unwrap();
5996        let SetProgramOutcome::Success { exec_outcome, .. } = set_program_outcome else {
5997            panic!("Expected successful baseline program execution");
5998        };
5999
6000        clear_mem_cache().await;
6001        assert!(read_old_memory().await.is_none());
6002
6003        let checkpoint_without_mock_memory = frontend
6004            .create_sketch_checkpoint((*exec_outcome).clone())
6005            .await
6006            .unwrap();
6007
6008        write_old_memory(SketchModeState::new_for_tests()).await;
6009        assert!(read_old_memory().await.is_some());
6010
6011        let checkpoint_with_mock_memory = frontend
6012            .create_sketch_checkpoint((*exec_outcome).clone())
6013            .await
6014            .unwrap();
6015
6016        clear_mem_cache().await;
6017        assert!(read_old_memory().await.is_none());
6018
6019        frontend
6020            .restore_sketch_checkpoint(checkpoint_with_mock_memory)
6021            .await
6022            .unwrap();
6023        assert!(read_old_memory().await.is_some());
6024
6025        frontend
6026            .restore_sketch_checkpoint(checkpoint_without_mock_memory)
6027            .await
6028            .unwrap();
6029        assert!(read_old_memory().await.is_none());
6030
6031        ctx.close().await;
6032    }
6033
6034    #[tokio::test(flavor = "multi_thread")]
6035    async fn test_hack_set_program_exec_error_still_allows_edit_sketch() {
6036        let source = "\
6037sketch(on = XY) {
6038  line1 = line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])
6039}
6040
6041bad = missing_name
6042";
6043        let program = Program::parse(source).unwrap().0.unwrap();
6044
6045        let mut frontend = FrontendState::new();
6046
6047        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6048        let mock_ctx = ExecutorContext::new_mock(None).await;
6049        let version = Version(0);
6050        let project_id = ProjectId(0);
6051        let file_id = FileId(0);
6052
6053        let SetProgramOutcome::ExecFailure { .. } = frontend.hack_set_program(&ctx, program).await.unwrap() else {
6054            panic!("Expected ExecFailure from hack_set_program due to syntax error in program");
6055        };
6056
6057        let sketch_id = frontend
6058            .scene_graph
6059            .objects
6060            .iter()
6061            .find_map(|obj| matches!(obj.kind, ObjectKind::Sketch(_)).then_some(obj.id))
6062            .expect("Expected sketch object from errored hack_set_program");
6063
6064        frontend
6065            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
6066            .await
6067            .unwrap();
6068
6069        ctx.close().await;
6070        mock_ctx.close().await;
6071    }
6072
6073    #[tokio::test(flavor = "multi_thread")]
6074    async fn test_new_sketch_add_point_edit_point() {
6075        let program = Program::empty();
6076
6077        let mut frontend = FrontendState::new();
6078        frontend.program = program;
6079
6080        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6081        let mock_ctx = ExecutorContext::new_mock(None).await;
6082        let version = Version(0);
6083
6084        let sketch_args = SketchCtor {
6085            on: Plane::Default(PlaneName::Xy),
6086        };
6087        let (_src_delta, scene_delta, sketch_id) = frontend
6088            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
6089            .await
6090            .unwrap();
6091        assert_eq!(sketch_id, ObjectId(1));
6092        assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
6093        let sketch_object = &scene_delta.new_graph.objects[1];
6094        assert_eq!(sketch_object.id, ObjectId(1));
6095        assert_eq!(
6096            sketch_object.kind,
6097            ObjectKind::Sketch(Sketch {
6098                args: SketchCtor {
6099                    on: Plane::Default(PlaneName::Xy)
6100                },
6101                plane: ObjectId(0),
6102                segments: vec![],
6103                constraints: vec![],
6104            })
6105        );
6106        assert_eq!(scene_delta.new_graph.objects.len(), 2);
6107
6108        let point_ctor = PointCtor {
6109            position: Point2d {
6110                x: Expr::Number(Number {
6111                    value: 1.0,
6112                    units: NumericSuffix::Inch,
6113                }),
6114                y: Expr::Number(Number {
6115                    value: 2.0,
6116                    units: NumericSuffix::Inch,
6117                }),
6118            },
6119        };
6120        let segment = SegmentCtor::Point(point_ctor);
6121        let (src_delta, scene_delta) = frontend
6122            .add_segment(&mock_ctx, version, sketch_id, segment, None)
6123            .await
6124            .unwrap();
6125        assert_eq!(
6126            src_delta.text.as_str(),
6127            "sketch001 = sketch(on = XY) {
6128  point(at = [1in, 2in])
6129}
6130"
6131        );
6132        assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
6133        assert_eq!(scene_delta.new_graph.objects.len(), 3);
6134        for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
6135            assert_eq!(scene_object.id.0, i);
6136        }
6137
6138        let point_id = *scene_delta.new_objects.last().unwrap();
6139
6140        let point_ctor = PointCtor {
6141            position: Point2d {
6142                x: Expr::Number(Number {
6143                    value: 3.0,
6144                    units: NumericSuffix::Inch,
6145                }),
6146                y: Expr::Number(Number {
6147                    value: 4.0,
6148                    units: NumericSuffix::Inch,
6149                }),
6150            },
6151        };
6152        let segments = vec![ExistingSegmentCtor {
6153            id: point_id,
6154            ctor: SegmentCtor::Point(point_ctor),
6155        }];
6156        let (src_delta, scene_delta) = frontend
6157            .edit_segments(&mock_ctx, version, sketch_id, segments)
6158            .await
6159            .unwrap();
6160        assert_eq!(
6161            src_delta.text.as_str(),
6162            "sketch001 = sketch(on = XY) {
6163  point(at = [3in, 4in])
6164}
6165"
6166        );
6167        assert_eq!(scene_delta.new_objects, vec![]);
6168        assert_eq!(scene_delta.new_graph.objects.len(), 3);
6169
6170        ctx.close().await;
6171        mock_ctx.close().await;
6172    }
6173
6174    #[tokio::test(flavor = "multi_thread")]
6175    async fn test_new_sketch_add_line_edit_line() {
6176        let program = Program::empty();
6177
6178        let mut frontend = FrontendState::new();
6179        frontend.program = program;
6180
6181        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6182        let mock_ctx = ExecutorContext::new_mock(None).await;
6183        let version = Version(0);
6184
6185        let sketch_args = SketchCtor {
6186            on: Plane::Default(PlaneName::Xy),
6187        };
6188        let (_src_delta, scene_delta, sketch_id) = frontend
6189            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
6190            .await
6191            .unwrap();
6192        assert_eq!(sketch_id, ObjectId(1));
6193        assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
6194        let sketch_object = &scene_delta.new_graph.objects[1];
6195        assert_eq!(sketch_object.id, ObjectId(1));
6196        assert_eq!(
6197            sketch_object.kind,
6198            ObjectKind::Sketch(Sketch {
6199                args: SketchCtor {
6200                    on: Plane::Default(PlaneName::Xy)
6201                },
6202                plane: ObjectId(0),
6203                segments: vec![],
6204                constraints: vec![],
6205            })
6206        );
6207        assert_eq!(scene_delta.new_graph.objects.len(), 2);
6208
6209        let line_ctor = LineCtor {
6210            start: Point2d {
6211                x: Expr::Number(Number {
6212                    value: 0.0,
6213                    units: NumericSuffix::Mm,
6214                }),
6215                y: Expr::Number(Number {
6216                    value: 0.0,
6217                    units: NumericSuffix::Mm,
6218                }),
6219            },
6220            end: Point2d {
6221                x: Expr::Number(Number {
6222                    value: 10.0,
6223                    units: NumericSuffix::Mm,
6224                }),
6225                y: Expr::Number(Number {
6226                    value: 10.0,
6227                    units: NumericSuffix::Mm,
6228                }),
6229            },
6230            construction: None,
6231        };
6232        let segment = SegmentCtor::Line(line_ctor);
6233        let (src_delta, scene_delta) = frontend
6234            .add_segment(&mock_ctx, version, sketch_id, segment, None)
6235            .await
6236            .unwrap();
6237        assert_eq!(
6238            src_delta.text.as_str(),
6239            "sketch001 = sketch(on = XY) {
6240  line(start = [0mm, 0mm], end = [10mm, 10mm])
6241}
6242"
6243        );
6244        assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
6245        assert_eq!(scene_delta.new_graph.objects.len(), 5);
6246        for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
6247            assert_eq!(scene_object.id.0, i);
6248        }
6249
6250        // The new objects are the end points and then the line.
6251        let line = *scene_delta.new_objects.last().unwrap();
6252
6253        let line_ctor = LineCtor {
6254            start: Point2d {
6255                x: Expr::Number(Number {
6256                    value: 1.0,
6257                    units: NumericSuffix::Mm,
6258                }),
6259                y: Expr::Number(Number {
6260                    value: 2.0,
6261                    units: NumericSuffix::Mm,
6262                }),
6263            },
6264            end: Point2d {
6265                x: Expr::Number(Number {
6266                    value: 13.0,
6267                    units: NumericSuffix::Mm,
6268                }),
6269                y: Expr::Number(Number {
6270                    value: 14.0,
6271                    units: NumericSuffix::Mm,
6272                }),
6273            },
6274            construction: None,
6275        };
6276        let segments = vec![ExistingSegmentCtor {
6277            id: line,
6278            ctor: SegmentCtor::Line(line_ctor),
6279        }];
6280        let (src_delta, scene_delta) = frontend
6281            .edit_segments(&mock_ctx, version, sketch_id, segments)
6282            .await
6283            .unwrap();
6284        assert_eq!(
6285            src_delta.text.as_str(),
6286            "sketch001 = sketch(on = XY) {
6287  line(start = [1mm, 2mm], end = [13mm, 14mm])
6288}
6289"
6290        );
6291        assert_eq!(scene_delta.new_objects, vec![]);
6292        assert_eq!(scene_delta.new_graph.objects.len(), 5);
6293
6294        ctx.close().await;
6295        mock_ctx.close().await;
6296    }
6297
6298    #[tokio::test(flavor = "multi_thread")]
6299    async fn test_new_sketch_add_arc_edit_arc() {
6300        let program = Program::empty();
6301
6302        let mut frontend = FrontendState::new();
6303        frontend.program = program;
6304
6305        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6306        let mock_ctx = ExecutorContext::new_mock(None).await;
6307        let version = Version(0);
6308
6309        let sketch_args = SketchCtor {
6310            on: Plane::Default(PlaneName::Xy),
6311        };
6312        let (_src_delta, scene_delta, sketch_id) = frontend
6313            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
6314            .await
6315            .unwrap();
6316        assert_eq!(sketch_id, ObjectId(1));
6317        assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
6318        let sketch_object = &scene_delta.new_graph.objects[1];
6319        assert_eq!(sketch_object.id, ObjectId(1));
6320        assert_eq!(
6321            sketch_object.kind,
6322            ObjectKind::Sketch(Sketch {
6323                args: SketchCtor {
6324                    on: Plane::Default(PlaneName::Xy),
6325                },
6326                plane: ObjectId(0),
6327                segments: vec![],
6328                constraints: vec![],
6329            })
6330        );
6331        assert_eq!(scene_delta.new_graph.objects.len(), 2);
6332
6333        let arc_ctor = ArcCtor {
6334            start: Point2d {
6335                x: Expr::Var(Number {
6336                    value: 0.0,
6337                    units: NumericSuffix::Mm,
6338                }),
6339                y: Expr::Var(Number {
6340                    value: 0.0,
6341                    units: NumericSuffix::Mm,
6342                }),
6343            },
6344            end: Point2d {
6345                x: Expr::Var(Number {
6346                    value: 10.0,
6347                    units: NumericSuffix::Mm,
6348                }),
6349                y: Expr::Var(Number {
6350                    value: 10.0,
6351                    units: NumericSuffix::Mm,
6352                }),
6353            },
6354            center: Point2d {
6355                x: Expr::Var(Number {
6356                    value: 10.0,
6357                    units: NumericSuffix::Mm,
6358                }),
6359                y: Expr::Var(Number {
6360                    value: 0.0,
6361                    units: NumericSuffix::Mm,
6362                }),
6363            },
6364            construction: None,
6365        };
6366        let segment = SegmentCtor::Arc(arc_ctor);
6367        let (src_delta, scene_delta) = frontend
6368            .add_segment(&mock_ctx, version, sketch_id, segment, None)
6369            .await
6370            .unwrap();
6371        assert_eq!(
6372            src_delta.text.as_str(),
6373            "sketch001 = sketch(on = XY) {
6374  arc(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm], center = [var 10mm, var 0mm])
6375}
6376"
6377        );
6378        assert_eq!(
6379            scene_delta.new_objects,
6380            vec![ObjectId(2), ObjectId(3), ObjectId(4), ObjectId(5)]
6381        );
6382        for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
6383            assert_eq!(scene_object.id.0, i);
6384        }
6385        assert_eq!(scene_delta.new_graph.objects.len(), 6);
6386
6387        // The new objects are the end points, the center, and then the arc.
6388        let arc = *scene_delta.new_objects.last().unwrap();
6389
6390        let arc_ctor = ArcCtor {
6391            start: Point2d {
6392                x: Expr::Var(Number {
6393                    value: 1.0,
6394                    units: NumericSuffix::Mm,
6395                }),
6396                y: Expr::Var(Number {
6397                    value: 2.0,
6398                    units: NumericSuffix::Mm,
6399                }),
6400            },
6401            end: Point2d {
6402                x: Expr::Var(Number {
6403                    value: 13.0,
6404                    units: NumericSuffix::Mm,
6405                }),
6406                y: Expr::Var(Number {
6407                    value: 14.0,
6408                    units: NumericSuffix::Mm,
6409                }),
6410            },
6411            center: Point2d {
6412                x: Expr::Var(Number {
6413                    value: 13.0,
6414                    units: NumericSuffix::Mm,
6415                }),
6416                y: Expr::Var(Number {
6417                    value: 2.0,
6418                    units: NumericSuffix::Mm,
6419                }),
6420            },
6421            construction: None,
6422        };
6423        let segments = vec![ExistingSegmentCtor {
6424            id: arc,
6425            ctor: SegmentCtor::Arc(arc_ctor),
6426        }];
6427        let (src_delta, scene_delta) = frontend
6428            .edit_segments(&mock_ctx, version, sketch_id, segments)
6429            .await
6430            .unwrap();
6431        assert_eq!(
6432            src_delta.text.as_str(),
6433            "sketch001 = sketch(on = XY) {
6434  arc(start = [var 1mm, var 2mm], end = [var 13mm, var 14mm], center = [var 13mm, var 2mm])
6435}
6436"
6437        );
6438        assert_eq!(scene_delta.new_objects, vec![]);
6439        assert_eq!(scene_delta.new_graph.objects.len(), 6);
6440
6441        ctx.close().await;
6442        mock_ctx.close().await;
6443    }
6444
6445    #[tokio::test(flavor = "multi_thread")]
6446    async fn test_new_sketch_add_circle_edit_circle() {
6447        let program = Program::empty();
6448
6449        let mut frontend = FrontendState::new();
6450        frontend.program = program;
6451
6452        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6453        let mock_ctx = ExecutorContext::new_mock(None).await;
6454        let version = Version(0);
6455
6456        let sketch_args = SketchCtor {
6457            on: Plane::Default(PlaneName::Xy),
6458        };
6459        let (_src_delta, _scene_delta, sketch_id) = frontend
6460            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
6461            .await
6462            .unwrap();
6463
6464        // Add a circle segment.
6465        let circle_ctor = CircleCtor {
6466            start: Point2d {
6467                x: Expr::Var(Number {
6468                    value: 5.0,
6469                    units: NumericSuffix::Mm,
6470                }),
6471                y: Expr::Var(Number {
6472                    value: 0.0,
6473                    units: NumericSuffix::Mm,
6474                }),
6475            },
6476            center: Point2d {
6477                x: Expr::Var(Number {
6478                    value: 0.0,
6479                    units: NumericSuffix::Mm,
6480                }),
6481                y: Expr::Var(Number {
6482                    value: 0.0,
6483                    units: NumericSuffix::Mm,
6484                }),
6485            },
6486            construction: None,
6487        };
6488        let segment = SegmentCtor::Circle(circle_ctor);
6489        let (src_delta, scene_delta) = frontend
6490            .add_segment(&mock_ctx, version, sketch_id, segment, None)
6491            .await
6492            .unwrap();
6493        assert_eq!(
6494            src_delta.text.as_str(),
6495            "sketch001 = sketch(on = XY) {
6496  circle1 = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
6497}
6498"
6499        );
6500        // The new objects are start, center, and then the circle segment.
6501        assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
6502        assert_eq!(scene_delta.new_graph.objects.len(), 5);
6503
6504        let circle = *scene_delta.new_objects.last().unwrap();
6505
6506        // Edit the circle segment.
6507        let circle_ctor = CircleCtor {
6508            start: Point2d {
6509                x: Expr::Var(Number {
6510                    value: 10.0,
6511                    units: NumericSuffix::Mm,
6512                }),
6513                y: Expr::Var(Number {
6514                    value: 0.0,
6515                    units: NumericSuffix::Mm,
6516                }),
6517            },
6518            center: Point2d {
6519                x: Expr::Var(Number {
6520                    value: 3.0,
6521                    units: NumericSuffix::Mm,
6522                }),
6523                y: Expr::Var(Number {
6524                    value: 4.0,
6525                    units: NumericSuffix::Mm,
6526                }),
6527            },
6528            construction: None,
6529        };
6530        let segments = vec![ExistingSegmentCtor {
6531            id: circle,
6532            ctor: SegmentCtor::Circle(circle_ctor),
6533        }];
6534        let (src_delta, scene_delta) = frontend
6535            .edit_segments(&mock_ctx, version, sketch_id, segments)
6536            .await
6537            .unwrap();
6538        assert_eq!(
6539            src_delta.text.as_str(),
6540            "sketch001 = sketch(on = XY) {
6541  circle1 = circle(start = [var 10mm, var 0mm], center = [var 3mm, var 4mm])
6542}
6543"
6544        );
6545        assert_eq!(scene_delta.new_objects, vec![]);
6546        assert_eq!(scene_delta.new_graph.objects.len(), 5);
6547
6548        ctx.close().await;
6549        mock_ctx.close().await;
6550    }
6551
6552    #[tokio::test(flavor = "multi_thread")]
6553    async fn test_delete_circle() {
6554        let initial_source = "sketch001 = sketch(on = XY) {
6555  circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
6556}
6557";
6558
6559        let program = Program::parse(initial_source).unwrap().0.unwrap();
6560        let mut frontend = FrontendState::new();
6561
6562        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6563        let mock_ctx = ExecutorContext::new_mock(None).await;
6564        let version = Version(0);
6565
6566        frontend.hack_set_program(&ctx, program).await.unwrap();
6567        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
6568        let sketch_id = sketch_object.id;
6569        let sketch = expect_sketch(sketch_object);
6570
6571        // The sketch should have 3 segments: start point, center point, and the circle.
6572        assert_eq!(sketch.segments.len(), 3);
6573        let circle_id = sketch.segments[2];
6574
6575        // Delete the circle.
6576        let (src_delta, scene_delta) = frontend
6577            .delete_objects(&mock_ctx, version, sketch_id, vec![], vec![circle_id])
6578            .await
6579            .unwrap();
6580        assert_eq!(
6581            src_delta.text.as_str(),
6582            "sketch001 = sketch(on = XY) {
6583}
6584"
6585        );
6586        let new_sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
6587        let new_sketch = expect_sketch(new_sketch_object);
6588        assert_eq!(new_sketch.segments.len(), 0);
6589
6590        ctx.close().await;
6591        mock_ctx.close().await;
6592    }
6593
6594    #[tokio::test(flavor = "multi_thread")]
6595    async fn test_edit_circle_via_point() {
6596        let initial_source = "sketch001 = sketch(on = XY) {
6597  circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
6598}
6599";
6600
6601        let program = Program::parse(initial_source).unwrap().0.unwrap();
6602        let mut frontend = FrontendState::new();
6603
6604        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6605        let mock_ctx = ExecutorContext::new_mock(None).await;
6606        let version = Version(0);
6607
6608        frontend.hack_set_program(&ctx, program).await.unwrap();
6609        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
6610        let sketch_id = sketch_object.id;
6611        let sketch = expect_sketch(sketch_object);
6612
6613        // Find the circle segment and its start point.
6614        let circle_id = sketch
6615            .segments
6616            .iter()
6617            .copied()
6618            .find(|seg_id| {
6619                matches!(
6620                    &frontend.scene_graph.objects[seg_id.0].kind,
6621                    ObjectKind::Segment {
6622                        segment: Segment::Circle(_)
6623                    }
6624                )
6625            })
6626            .expect("Expected a circle segment in sketch");
6627        let circle_object = &frontend.scene_graph.objects[circle_id.0];
6628        let ObjectKind::Segment {
6629            segment: Segment::Circle(circle),
6630        } = &circle_object.kind
6631        else {
6632            panic!("Expected circle segment, got: {:?}", circle_object.kind);
6633        };
6634        let start_point_id = circle.start;
6635
6636        // Edit the start point via SegmentCtor::Point.
6637        let segments = vec![ExistingSegmentCtor {
6638            id: start_point_id,
6639            ctor: SegmentCtor::Point(PointCtor {
6640                position: Point2d {
6641                    x: Expr::Var(Number {
6642                        value: 7.0,
6643                        units: NumericSuffix::Mm,
6644                    }),
6645                    y: Expr::Var(Number {
6646                        value: 1.0,
6647                        units: NumericSuffix::Mm,
6648                    }),
6649                },
6650            }),
6651        }];
6652        let (src_delta, _scene_delta) = frontend
6653            .edit_segments(&mock_ctx, version, sketch_id, segments)
6654            .await
6655            .unwrap();
6656        assert_eq!(
6657            src_delta.text.as_str(),
6658            "sketch001 = sketch(on = XY) {
6659  circle(start = [var 7mm, var 1mm], center = [var 0mm, var 0mm])
6660}
6661"
6662        );
6663
6664        ctx.close().await;
6665        mock_ctx.close().await;
6666    }
6667
6668    #[tokio::test(flavor = "multi_thread")]
6669    async fn test_add_line_when_sketch_block_uses_variable() {
6670        let initial_source = "s = sketch(on = XY) {}
6671";
6672
6673        let program = Program::parse(initial_source).unwrap().0.unwrap();
6674
6675        let mut frontend = FrontendState::new();
6676
6677        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6678        let mock_ctx = ExecutorContext::new_mock(None).await;
6679        let version = Version(0);
6680
6681        frontend.hack_set_program(&ctx, program).await.unwrap();
6682        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
6683        let sketch_id = sketch_object.id;
6684
6685        let line_ctor = LineCtor {
6686            start: Point2d {
6687                x: Expr::Number(Number {
6688                    value: 0.0,
6689                    units: NumericSuffix::Mm,
6690                }),
6691                y: Expr::Number(Number {
6692                    value: 0.0,
6693                    units: NumericSuffix::Mm,
6694                }),
6695            },
6696            end: Point2d {
6697                x: Expr::Number(Number {
6698                    value: 10.0,
6699                    units: NumericSuffix::Mm,
6700                }),
6701                y: Expr::Number(Number {
6702                    value: 10.0,
6703                    units: NumericSuffix::Mm,
6704                }),
6705            },
6706            construction: None,
6707        };
6708        let segment = SegmentCtor::Line(line_ctor);
6709        let (src_delta, scene_delta) = frontend
6710            .add_segment(&mock_ctx, version, sketch_id, segment, None)
6711            .await
6712            .unwrap();
6713        assert_eq!(
6714            src_delta.text.as_str(),
6715            "s = sketch(on = XY) {
6716  line(start = [0mm, 0mm], end = [10mm, 10mm])
6717}
6718"
6719        );
6720        assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
6721        assert_eq!(scene_delta.new_graph.objects.len(), 5);
6722
6723        ctx.close().await;
6724        mock_ctx.close().await;
6725    }
6726
6727    #[tokio::test(flavor = "multi_thread")]
6728    async fn test_new_sketch_add_line_delete_sketch() {
6729        let program = Program::empty();
6730
6731        let mut frontend = FrontendState::new();
6732        frontend.program = program;
6733
6734        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6735        let mock_ctx = ExecutorContext::new_mock(None).await;
6736        let version = Version(0);
6737
6738        let sketch_args = SketchCtor {
6739            on: Plane::Default(PlaneName::Xy),
6740        };
6741        let (_src_delta, scene_delta, sketch_id) = frontend
6742            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
6743            .await
6744            .unwrap();
6745        assert_eq!(sketch_id, ObjectId(1));
6746        assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
6747        let sketch_object = &scene_delta.new_graph.objects[1];
6748        assert_eq!(sketch_object.id, ObjectId(1));
6749        assert_eq!(
6750            sketch_object.kind,
6751            ObjectKind::Sketch(Sketch {
6752                args: SketchCtor {
6753                    on: Plane::Default(PlaneName::Xy)
6754                },
6755                plane: ObjectId(0),
6756                segments: vec![],
6757                constraints: vec![],
6758            })
6759        );
6760        assert_eq!(scene_delta.new_graph.objects.len(), 2);
6761
6762        let line_ctor = LineCtor {
6763            start: Point2d {
6764                x: Expr::Number(Number {
6765                    value: 0.0,
6766                    units: NumericSuffix::Mm,
6767                }),
6768                y: Expr::Number(Number {
6769                    value: 0.0,
6770                    units: NumericSuffix::Mm,
6771                }),
6772            },
6773            end: Point2d {
6774                x: Expr::Number(Number {
6775                    value: 10.0,
6776                    units: NumericSuffix::Mm,
6777                }),
6778                y: Expr::Number(Number {
6779                    value: 10.0,
6780                    units: NumericSuffix::Mm,
6781                }),
6782            },
6783            construction: None,
6784        };
6785        let segment = SegmentCtor::Line(line_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  line(start = [0mm, 0mm], end = [10mm, 10mm])
6794}
6795"
6796        );
6797        assert_eq!(scene_delta.new_graph.objects.len(), 5);
6798
6799        let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
6800        assert_eq!(src_delta.text.as_str(), "");
6801        assert_eq!(scene_delta.new_graph.objects.len(), 0);
6802
6803        ctx.close().await;
6804        mock_ctx.close().await;
6805    }
6806
6807    #[tokio::test(flavor = "multi_thread")]
6808    async fn test_delete_sketch_when_sketch_block_uses_variable() {
6809        let initial_source = "s = sketch(on = XY) {}
6810";
6811
6812        let program = Program::parse(initial_source).unwrap().0.unwrap();
6813
6814        let mut frontend = FrontendState::new();
6815
6816        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6817        let mock_ctx = ExecutorContext::new_mock(None).await;
6818        let version = Version(0);
6819
6820        frontend.hack_set_program(&ctx, program).await.unwrap();
6821        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
6822        let sketch_id = sketch_object.id;
6823
6824        let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
6825        assert_eq!(src_delta.text.as_str(), "");
6826        assert_eq!(scene_delta.new_graph.objects.len(), 0);
6827
6828        ctx.close().await;
6829        mock_ctx.close().await;
6830    }
6831
6832    #[tokio::test(flavor = "multi_thread")]
6833    async fn test_edit_line_when_editing_its_start_point() {
6834        let initial_source = "\
6835sketch(on = XY) {
6836  line(start = [var 1, var 2], end = [var 3, var 4])
6837}
6838";
6839
6840        let program = Program::parse(initial_source).unwrap().0.unwrap();
6841
6842        let mut frontend = FrontendState::new();
6843
6844        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6845        let mock_ctx = ExecutorContext::new_mock(None).await;
6846        let version = Version(0);
6847
6848        frontend.hack_set_program(&ctx, program).await.unwrap();
6849        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
6850        let sketch_id = sketch_object.id;
6851        let sketch = expect_sketch(sketch_object);
6852
6853        let point_id = *sketch.segments.first().unwrap();
6854
6855        let point_ctor = PointCtor {
6856            position: Point2d {
6857                x: Expr::Var(Number {
6858                    value: 5.0,
6859                    units: NumericSuffix::Inch,
6860                }),
6861                y: Expr::Var(Number {
6862                    value: 6.0,
6863                    units: NumericSuffix::Inch,
6864                }),
6865            },
6866        };
6867        let segments = vec![ExistingSegmentCtor {
6868            id: point_id,
6869            ctor: SegmentCtor::Point(point_ctor),
6870        }];
6871        let (src_delta, scene_delta) = frontend
6872            .edit_segments(&mock_ctx, version, sketch_id, segments)
6873            .await
6874            .unwrap();
6875        assert_eq!(
6876            src_delta.text.as_str(),
6877            "\
6878sketch(on = XY) {
6879  line(start = [var 127mm, var 152.4mm], end = [var 3mm, var 4mm])
6880}
6881"
6882        );
6883        assert_eq!(scene_delta.new_objects, vec![]);
6884        assert_eq!(scene_delta.new_graph.objects.len(), 5);
6885
6886        ctx.close().await;
6887        mock_ctx.close().await;
6888    }
6889
6890    #[tokio::test(flavor = "multi_thread")]
6891    async fn test_edit_line_when_editing_its_end_point() {
6892        let initial_source = "\
6893sketch(on = XY) {
6894  line(start = [var 1, var 2], end = [var 3, var 4])
6895}
6896";
6897
6898        let program = Program::parse(initial_source).unwrap().0.unwrap();
6899
6900        let mut frontend = FrontendState::new();
6901
6902        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6903        let mock_ctx = ExecutorContext::new_mock(None).await;
6904        let version = Version(0);
6905
6906        frontend.hack_set_program(&ctx, program).await.unwrap();
6907        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
6908        let sketch_id = sketch_object.id;
6909        let sketch = expect_sketch(sketch_object);
6910        let point_id = *sketch.segments.get(1).unwrap();
6911
6912        let point_ctor = PointCtor {
6913            position: Point2d {
6914                x: Expr::Var(Number {
6915                    value: 5.0,
6916                    units: NumericSuffix::Inch,
6917                }),
6918                y: Expr::Var(Number {
6919                    value: 6.0,
6920                    units: NumericSuffix::Inch,
6921                }),
6922            },
6923        };
6924        let segments = vec![ExistingSegmentCtor {
6925            id: point_id,
6926            ctor: SegmentCtor::Point(point_ctor),
6927        }];
6928        let (src_delta, scene_delta) = frontend
6929            .edit_segments(&mock_ctx, version, sketch_id, segments)
6930            .await
6931            .unwrap();
6932        assert_eq!(
6933            src_delta.text.as_str(),
6934            "\
6935sketch(on = XY) {
6936  line(start = [var 1mm, var 2mm], end = [var 127mm, var 152.4mm])
6937}
6938"
6939        );
6940        assert_eq!(scene_delta.new_objects, vec![]);
6941        assert_eq!(
6942            scene_delta.new_graph.objects.len(),
6943            5,
6944            "{:#?}",
6945            scene_delta.new_graph.objects
6946        );
6947
6948        ctx.close().await;
6949        mock_ctx.close().await;
6950    }
6951
6952    #[tokio::test(flavor = "multi_thread")]
6953    async fn test_edit_line_with_coincident_feedback() {
6954        let initial_source = "\
6955sketch(on = XY) {
6956  line1 = line(start = [var 1, var 2], end = [var 1, var 2])
6957  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
6958  fixed([line1.start, [0, 0]])
6959  coincident([line1.end, line2.start])
6960  equalLength([line1, line2])
6961}
6962";
6963
6964        let program = Program::parse(initial_source).unwrap().0.unwrap();
6965
6966        let mut frontend = FrontendState::new();
6967
6968        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6969        let mock_ctx = ExecutorContext::new_mock(None).await;
6970        let version = Version(0);
6971
6972        frontend.hack_set_program(&ctx, program).await.unwrap();
6973        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
6974        let sketch_id = sketch_object.id;
6975        let sketch = expect_sketch(sketch_object);
6976        let line2_end_id = *sketch.segments.get(4).unwrap();
6977
6978        let segments = vec![ExistingSegmentCtor {
6979            id: line2_end_id,
6980            ctor: SegmentCtor::Point(PointCtor {
6981                position: Point2d {
6982                    x: Expr::Var(Number {
6983                        value: 9.0,
6984                        units: NumericSuffix::None,
6985                    }),
6986                    y: Expr::Var(Number {
6987                        value: 10.0,
6988                        units: NumericSuffix::None,
6989                    }),
6990                },
6991            }),
6992        }];
6993        let (src_delta, scene_delta) = frontend
6994            .edit_segments(&mock_ctx, version, sketch_id, segments)
6995            .await
6996            .unwrap();
6997        assert_eq!(
6998            src_delta.text.as_str(),
6999            "\
7000sketch(on = XY) {
7001  line1 = line(start = [var 0mm, var 0mm], end = [var 4.14mm, var 5.32mm])
7002  line2 = line(start = [var 4.14mm, var 5.32mm], end = [var 9mm, var 10mm])
7003  fixed([line1.start, [0, 0]])
7004  coincident([line1.end, line2.start])
7005  equalLength([line1, line2])
7006}
7007"
7008        );
7009        assert_eq!(
7010            scene_delta.new_graph.objects.len(),
7011            11,
7012            "{:#?}",
7013            scene_delta.new_graph.objects
7014        );
7015
7016        ctx.close().await;
7017        mock_ctx.close().await;
7018    }
7019
7020    #[tokio::test(flavor = "multi_thread")]
7021    async fn test_delete_point_without_var() {
7022        let initial_source = "\
7023sketch(on = XY) {
7024  point(at = [var 1, var 2])
7025  point(at = [var 3, var 4])
7026  point(at = [var 5, var 6])
7027}
7028";
7029
7030        let program = Program::parse(initial_source).unwrap().0.unwrap();
7031
7032        let mut frontend = FrontendState::new();
7033
7034        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7035        let mock_ctx = ExecutorContext::new_mock(None).await;
7036        let version = Version(0);
7037
7038        frontend.hack_set_program(&ctx, program).await.unwrap();
7039        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7040        let sketch_id = sketch_object.id;
7041        let sketch = expect_sketch(sketch_object);
7042
7043        let point_id = *sketch.segments.get(1).unwrap();
7044
7045        let (src_delta, scene_delta) = frontend
7046            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
7047            .await
7048            .unwrap();
7049        assert_eq!(
7050            src_delta.text.as_str(),
7051            "\
7052sketch(on = XY) {
7053  point(at = [var 1mm, var 2mm])
7054  point(at = [var 5mm, var 6mm])
7055}
7056"
7057        );
7058        assert_eq!(scene_delta.new_objects, vec![]);
7059        assert_eq!(scene_delta.new_graph.objects.len(), 4);
7060
7061        ctx.close().await;
7062        mock_ctx.close().await;
7063    }
7064
7065    #[tokio::test(flavor = "multi_thread")]
7066    async fn test_delete_point_with_var() {
7067        let initial_source = "\
7068sketch(on = XY) {
7069  point(at = [var 1, var 2])
7070  point1 = point(at = [var 3, var 4])
7071  point(at = [var 5, var 6])
7072}
7073";
7074
7075        let program = Program::parse(initial_source).unwrap().0.unwrap();
7076
7077        let mut frontend = FrontendState::new();
7078
7079        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7080        let mock_ctx = ExecutorContext::new_mock(None).await;
7081        let version = Version(0);
7082
7083        frontend.hack_set_program(&ctx, program).await.unwrap();
7084        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7085        let sketch_id = sketch_object.id;
7086        let sketch = expect_sketch(sketch_object);
7087
7088        let point_id = *sketch.segments.get(1).unwrap();
7089
7090        let (src_delta, scene_delta) = frontend
7091            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
7092            .await
7093            .unwrap();
7094        assert_eq!(
7095            src_delta.text.as_str(),
7096            "\
7097sketch(on = XY) {
7098  point(at = [var 1mm, var 2mm])
7099  point(at = [var 5mm, var 6mm])
7100}
7101"
7102        );
7103        assert_eq!(scene_delta.new_objects, vec![]);
7104        assert_eq!(scene_delta.new_graph.objects.len(), 4);
7105
7106        ctx.close().await;
7107        mock_ctx.close().await;
7108    }
7109
7110    #[tokio::test(flavor = "multi_thread")]
7111    async fn test_delete_multiple_points() {
7112        let initial_source = "\
7113sketch(on = XY) {
7114  point(at = [var 1, var 2])
7115  point1 = point(at = [var 3, var 4])
7116  point(at = [var 5, var 6])
7117}
7118";
7119
7120        let program = Program::parse(initial_source).unwrap().0.unwrap();
7121
7122        let mut frontend = FrontendState::new();
7123
7124        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7125        let mock_ctx = ExecutorContext::new_mock(None).await;
7126        let version = Version(0);
7127
7128        frontend.hack_set_program(&ctx, program).await.unwrap();
7129        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7130        let sketch_id = sketch_object.id;
7131
7132        let sketch = expect_sketch(sketch_object);
7133
7134        let point1_id = *sketch.segments.first().unwrap();
7135        let point2_id = *sketch.segments.get(1).unwrap();
7136
7137        let (src_delta, scene_delta) = frontend
7138            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point1_id, point2_id])
7139            .await
7140            .unwrap();
7141        assert_eq!(
7142            src_delta.text.as_str(),
7143            "\
7144sketch(on = XY) {
7145  point(at = [var 5mm, var 6mm])
7146}
7147"
7148        );
7149        assert_eq!(scene_delta.new_objects, vec![]);
7150        assert_eq!(scene_delta.new_graph.objects.len(), 3);
7151
7152        ctx.close().await;
7153        mock_ctx.close().await;
7154    }
7155
7156    #[tokio::test(flavor = "multi_thread")]
7157    async fn test_delete_coincident_constraint() {
7158        let initial_source = "\
7159sketch(on = XY) {
7160  point1 = point(at = [var 1, var 2])
7161  point2 = point(at = [var 3, var 4])
7162  coincident([point1, point2])
7163  point(at = [var 5, var 6])
7164}
7165";
7166
7167        let program = Program::parse(initial_source).unwrap().0.unwrap();
7168
7169        let mut frontend = FrontendState::new();
7170
7171        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7172        let mock_ctx = ExecutorContext::new_mock(None).await;
7173        let version = Version(0);
7174
7175        frontend.hack_set_program(&ctx, program).await.unwrap();
7176        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7177        let sketch_id = sketch_object.id;
7178        let sketch = expect_sketch(sketch_object);
7179
7180        let coincident_id = *sketch.constraints.first().unwrap();
7181
7182        let (src_delta, scene_delta) = frontend
7183            .delete_objects(&mock_ctx, version, sketch_id, vec![coincident_id], Vec::new())
7184            .await
7185            .unwrap();
7186        assert_eq!(
7187            src_delta.text.as_str(),
7188            "\
7189sketch(on = XY) {
7190  point1 = point(at = [var 1mm, var 2mm])
7191  point2 = point(at = [var 3mm, var 4mm])
7192  point(at = [var 5mm, var 6mm])
7193}
7194"
7195        );
7196        assert_eq!(scene_delta.new_objects, vec![]);
7197        assert_eq!(scene_delta.new_graph.objects.len(), 5);
7198
7199        ctx.close().await;
7200        mock_ctx.close().await;
7201    }
7202
7203    #[tokio::test(flavor = "multi_thread")]
7204    async fn test_delete_line_cascades_to_coincident_constraint() {
7205        let initial_source = "\
7206sketch(on = XY) {
7207  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
7208  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
7209  coincident([line1.end, line2.start])
7210}
7211";
7212
7213        let program = Program::parse(initial_source).unwrap().0.unwrap();
7214
7215        let mut frontend = FrontendState::new();
7216
7217        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7218        let mock_ctx = ExecutorContext::new_mock(None).await;
7219        let version = Version(0);
7220
7221        frontend.hack_set_program(&ctx, program).await.unwrap();
7222        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7223        let sketch_id = sketch_object.id;
7224        let sketch = expect_sketch(sketch_object);
7225        let line_id = *sketch.segments.get(5).unwrap();
7226
7227        let (src_delta, scene_delta) = frontend
7228            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
7229            .await
7230            .unwrap();
7231        assert_eq!(
7232            src_delta.text.as_str(),
7233            "\
7234sketch(on = XY) {
7235  line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
7236}
7237"
7238        );
7239        assert_eq!(
7240            scene_delta.new_graph.objects.len(),
7241            5,
7242            "{:#?}",
7243            scene_delta.new_graph.objects
7244        );
7245
7246        ctx.close().await;
7247        mock_ctx.close().await;
7248    }
7249
7250    #[tokio::test(flavor = "multi_thread")]
7251    async fn test_delete_line_cascades_to_distance_constraint() {
7252        let initial_source = "\
7253sketch(on = XY) {
7254  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
7255  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
7256  distance([line1.end, line2.start]) == 10mm
7257}
7258";
7259
7260        let program = Program::parse(initial_source).unwrap().0.unwrap();
7261
7262        let mut frontend = FrontendState::new();
7263
7264        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7265        let mock_ctx = ExecutorContext::new_mock(None).await;
7266        let version = Version(0);
7267
7268        frontend.hack_set_program(&ctx, program).await.unwrap();
7269        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7270        let sketch_id = sketch_object.id;
7271        let sketch = expect_sketch(sketch_object);
7272        let line_id = *sketch.segments.get(5).unwrap();
7273
7274        let (src_delta, scene_delta) = frontend
7275            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
7276            .await
7277            .unwrap();
7278        assert_eq!(
7279            src_delta.text.as_str(),
7280            "\
7281sketch(on = XY) {
7282  line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
7283}
7284"
7285        );
7286        assert_eq!(
7287            scene_delta.new_graph.objects.len(),
7288            5,
7289            "{:#?}",
7290            scene_delta.new_graph.objects
7291        );
7292
7293        ctx.close().await;
7294        mock_ctx.close().await;
7295    }
7296
7297    #[tokio::test(flavor = "multi_thread")]
7298    async fn test_delete_point_preserves_multiline_coincident_constraint() {
7299        let initial_source = "\
7300sketch(on = XY) {
7301  point1 = point(at = [var 1, var 2])
7302  point2 = point(at = [var 3, var 4])
7303  point3 = point(at = [var 5, var 6])
7304  coincident([point1, point2, point3])
7305}
7306";
7307
7308        let program = Program::parse(initial_source).unwrap().0.unwrap();
7309
7310        let mut frontend = FrontendState::new();
7311
7312        let mock_ctx = ExecutorContext::new_mock(None).await;
7313        let version = Version(0);
7314
7315        frontend.program = program.clone();
7316        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
7317        frontend.update_state_after_exec(outcome, true);
7318        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7319        let sketch_id = sketch_object.id;
7320        let sketch = expect_sketch(sketch_object);
7321        let point3_id = *sketch.segments.get(2).unwrap();
7322
7323        let (src_delta, scene_delta) = frontend
7324            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point3_id])
7325            .await
7326            .unwrap();
7327        assert!(src_delta.text.contains("point1 = point("), "{}", src_delta.text);
7328        assert!(src_delta.text.contains("point2 = point("), "{}", src_delta.text);
7329        assert!(!src_delta.text.contains("point3 = point("), "{}", src_delta.text);
7330        assert!(
7331            src_delta.text.contains("coincident([point1, point2])"),
7332            "{}",
7333            src_delta.text
7334        );
7335
7336        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
7337        let sketch = expect_sketch(sketch_object);
7338        assert_eq!(sketch.segments.len(), 2);
7339        assert_eq!(sketch.constraints.len(), 1);
7340
7341        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
7342        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
7343            panic!("Expected constraint object");
7344        };
7345        let Constraint::Coincident(coincident) = constraint else {
7346            panic!("Expected coincident constraint");
7347        };
7348        assert_eq!(
7349            coincident.segments,
7350            sketch
7351                .segments
7352                .iter()
7353                .copied()
7354                .map(Into::into)
7355                .collect::<Vec<ConstraintSegment>>()
7356        );
7357
7358        mock_ctx.close().await;
7359    }
7360
7361    #[tokio::test(flavor = "multi_thread")]
7362    async fn test_delete_line_preserves_multiline_equal_length_constraint() {
7363        let initial_source = "\
7364sketch(on = XY) {
7365  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
7366  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
7367  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
7368  equalLength([line1, line2, line3])
7369}
7370";
7371
7372        let program = Program::parse(initial_source).unwrap().0.unwrap();
7373
7374        let mut frontend = FrontendState::new();
7375
7376        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7377        let mock_ctx = ExecutorContext::new_mock(None).await;
7378        let version = Version(0);
7379
7380        frontend.hack_set_program(&ctx, program).await.unwrap();
7381        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7382        let sketch_id = sketch_object.id;
7383        let sketch = expect_sketch(sketch_object);
7384        let line3_id = *sketch.segments.get(8).unwrap();
7385
7386        let (src_delta, scene_delta) = frontend
7387            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line3_id])
7388            .await
7389            .unwrap();
7390        assert_eq!(
7391            src_delta.text.as_str(),
7392            "\
7393sketch(on = XY) {
7394  line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
7395  line2 = line(start = [var 5mm, var 6mm], end = [var 7mm, var 8mm])
7396  equalLength([line1, line2])
7397}
7398"
7399        );
7400
7401        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
7402        let sketch = expect_sketch(sketch_object);
7403        assert_eq!(sketch.constraints.len(), 1);
7404
7405        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
7406        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
7407            panic!("Expected constraint object");
7408        };
7409        let Constraint::LinesEqualLength(lines_equal_length) = constraint else {
7410            panic!("Expected lines equal length constraint");
7411        };
7412        assert_eq!(lines_equal_length.lines.len(), 2);
7413
7414        ctx.close().await;
7415        mock_ctx.close().await;
7416    }
7417
7418    #[tokio::test(flavor = "multi_thread")]
7419    async fn test_delete_line_preserves_multiline_coincident_constraint() {
7420        let initial_source = "\
7421sketch(on = XY) {
7422  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
7423  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
7424  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
7425  coincident([line1.end, line2.start, line3.start])
7426}
7427";
7428
7429        let program = Program::parse(initial_source).unwrap().0.unwrap();
7430
7431        let mut frontend = FrontendState::new();
7432
7433        let mock_ctx = ExecutorContext::new_mock(None).await;
7434        let version = Version(0);
7435
7436        frontend.program = program.clone();
7437        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
7438        frontend.update_state_after_exec(outcome, true);
7439        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7440        let sketch_id = sketch_object.id;
7441        let sketch = expect_sketch(sketch_object);
7442        let line1_id = *sketch.segments.get(2).unwrap();
7443
7444        let (src_delta, scene_delta) = frontend
7445            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
7446            .await
7447            .unwrap();
7448        assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
7449        assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
7450        assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
7451        assert!(
7452            src_delta.text.contains("coincident([line2.start, line3.start])"),
7453            "{}",
7454            src_delta.text
7455        );
7456
7457        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
7458        let sketch = expect_sketch(sketch_object);
7459        assert_eq!(sketch.constraints.len(), 1);
7460
7461        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
7462        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
7463            panic!("Expected constraint object");
7464        };
7465        let Constraint::Coincident(coincident) = constraint else {
7466            panic!("Expected coincident constraint");
7467        };
7468        let remaining_segments = vec![sketch.segments[0].into(), sketch.segments[3].into()];
7469        assert_eq!(coincident.segments, remaining_segments);
7470
7471        mock_ctx.close().await;
7472    }
7473
7474    #[tokio::test(flavor = "multi_thread")]
7475    async fn test_delete_lines_removes_multiline_equal_length_constraint_below_minimum() {
7476        let initial_source = "\
7477sketch(on = XY) {
7478  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
7479  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
7480  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
7481  equalLength([line1, line2, line3])
7482}
7483";
7484
7485        let program = Program::parse(initial_source).unwrap().0.unwrap();
7486
7487        let mut frontend = FrontendState::new();
7488
7489        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7490        let mock_ctx = ExecutorContext::new_mock(None).await;
7491        let version = Version(0);
7492
7493        frontend.hack_set_program(&ctx, program).await.unwrap();
7494        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7495        let sketch_id = sketch_object.id;
7496        let sketch = expect_sketch(sketch_object);
7497        let line2_id = *sketch.segments.get(5).unwrap();
7498        let line3_id = *sketch.segments.get(8).unwrap();
7499
7500        let (src_delta, scene_delta) = frontend
7501            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line2_id, line3_id])
7502            .await
7503            .unwrap();
7504        assert_eq!(
7505            src_delta.text.as_str(),
7506            "\
7507sketch(on = XY) {
7508  line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
7509}
7510"
7511        );
7512
7513        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
7514        let sketch = expect_sketch(sketch_object);
7515        assert!(sketch.constraints.is_empty());
7516
7517        ctx.close().await;
7518        mock_ctx.close().await;
7519    }
7520
7521    #[tokio::test(flavor = "multi_thread")]
7522    async fn test_delete_line_preserves_multiline_parallel_constraint() {
7523        let initial_source = "\
7524sketch(on = XY) {
7525  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
7526  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
7527  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
7528  parallel([line1, line2, line3])
7529}
7530";
7531
7532        let program = Program::parse(initial_source).unwrap().0.unwrap();
7533
7534        let mut frontend = FrontendState::new();
7535
7536        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7537        let mock_ctx = ExecutorContext::new_mock(None).await;
7538        let version = Version(0);
7539
7540        frontend.hack_set_program(&ctx, program).await.unwrap();
7541        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7542        let sketch_id = sketch_object.id;
7543        let sketch = expect_sketch(sketch_object);
7544        let line3_id = *sketch.segments.get(8).unwrap();
7545
7546        let (src_delta, scene_delta) = frontend
7547            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line3_id])
7548            .await
7549            .unwrap();
7550        assert_eq!(
7551            src_delta.text.as_str(),
7552            "\
7553sketch(on = XY) {
7554  line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
7555  line2 = line(start = [var 5mm, var 6mm], end = [var 7mm, var 8mm])
7556  parallel([line1, line2])
7557}
7558"
7559        );
7560
7561        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
7562        let sketch = expect_sketch(sketch_object);
7563        assert_eq!(sketch.constraints.len(), 1);
7564
7565        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
7566        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
7567            panic!("Expected constraint object");
7568        };
7569        let Constraint::Parallel(parallel) = constraint else {
7570            panic!("Expected parallel constraint");
7571        };
7572        assert_eq!(parallel.lines.len(), 2);
7573
7574        ctx.close().await;
7575        mock_ctx.close().await;
7576    }
7577
7578    #[tokio::test(flavor = "multi_thread")]
7579    async fn test_delete_lines_removes_multiline_parallel_constraint_below_minimum() {
7580        let initial_source = "\
7581sketch(on = XY) {
7582  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
7583  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
7584  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
7585  parallel([line1, line2, line3])
7586}
7587";
7588
7589        let program = Program::parse(initial_source).unwrap().0.unwrap();
7590
7591        let mut frontend = FrontendState::new();
7592
7593        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7594        let mock_ctx = ExecutorContext::new_mock(None).await;
7595        let version = Version(0);
7596
7597        frontend.hack_set_program(&ctx, program).await.unwrap();
7598        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7599        let sketch_id = sketch_object.id;
7600        let sketch = expect_sketch(sketch_object);
7601        let line2_id = *sketch.segments.get(5).unwrap();
7602        let line3_id = *sketch.segments.get(8).unwrap();
7603
7604        let (src_delta, scene_delta) = frontend
7605            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line2_id, line3_id])
7606            .await
7607            .unwrap();
7608        assert_eq!(
7609            src_delta.text.as_str(),
7610            "\
7611sketch(on = XY) {
7612  line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
7613}
7614"
7615        );
7616
7617        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
7618        let sketch = expect_sketch(sketch_object);
7619        assert!(sketch.constraints.is_empty());
7620
7621        ctx.close().await;
7622        mock_ctx.close().await;
7623    }
7624
7625    #[tokio::test(flavor = "multi_thread")]
7626    async fn test_delete_line_line_coincident_constraint() {
7627        let initial_source = "\
7628sketch(on = XY) {
7629  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
7630  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
7631  coincident([line1, line2])
7632}
7633";
7634
7635        let program = Program::parse(initial_source).unwrap().0.unwrap();
7636
7637        let mut frontend = FrontendState::new();
7638
7639        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7640        let mock_ctx = ExecutorContext::new_mock(None).await;
7641        let version = Version(0);
7642
7643        frontend.hack_set_program(&ctx, program).await.unwrap();
7644        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7645        let sketch_id = sketch_object.id;
7646        let sketch = expect_sketch(sketch_object);
7647
7648        let coincident_id = *sketch.constraints.first().unwrap();
7649
7650        let (src_delta, scene_delta) = frontend
7651            .delete_objects(&mock_ctx, version, sketch_id, vec![coincident_id], Vec::new())
7652            .await
7653            .unwrap();
7654        assert_eq!(
7655            src_delta.text.as_str(),
7656            "\
7657sketch(on = XY) {
7658  line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
7659  line2 = line(start = [var 5mm, var 6mm], end = [var 7mm, var 8mm])
7660}
7661"
7662        );
7663        assert_eq!(scene_delta.new_objects, vec![]);
7664        assert_eq!(scene_delta.new_graph.objects.len(), 8);
7665
7666        ctx.close().await;
7667        mock_ctx.close().await;
7668    }
7669
7670    #[tokio::test(flavor = "multi_thread")]
7671    async fn test_two_points_coincident() {
7672        let initial_source = "\
7673sketch(on = XY) {
7674  point1 = point(at = [var 1, var 2])
7675  point(at = [3, 4])
7676}
7677";
7678
7679        let program = Program::parse(initial_source).unwrap().0.unwrap();
7680
7681        let mut frontend = FrontendState::new();
7682
7683        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7684        let mock_ctx = ExecutorContext::new_mock(None).await;
7685        let version = Version(0);
7686
7687        frontend.hack_set_program(&ctx, program).await.unwrap();
7688        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7689        let sketch_id = sketch_object.id;
7690        let sketch = expect_sketch(sketch_object);
7691        let point0_id = *sketch.segments.first().unwrap();
7692        let point1_id = *sketch.segments.get(1).unwrap();
7693
7694        let constraint = Constraint::Coincident(Coincident {
7695            segments: vec![point0_id.into(), point1_id.into()],
7696        });
7697        let (src_delta, scene_delta) = frontend
7698            .add_constraint(&mock_ctx, version, sketch_id, constraint)
7699            .await
7700            .unwrap();
7701        assert_eq!(
7702            src_delta.text.as_str(),
7703            "\
7704sketch(on = XY) {
7705  point1 = point(at = [var 1, var 2])
7706  point2 = point(at = [3, 4])
7707  coincident([point1, point2])
7708}
7709"
7710        );
7711        assert_eq!(
7712            scene_delta.new_graph.objects.len(),
7713            5,
7714            "{:#?}",
7715            scene_delta.new_graph.objects
7716        );
7717
7718        ctx.close().await;
7719        mock_ctx.close().await;
7720    }
7721
7722    #[tokio::test(flavor = "multi_thread")]
7723    async fn test_three_points_coincident() {
7724        let initial_source = "\
7725sketch(on = XY) {
7726  point1 = point(at = [var 1, var 2])
7727  point(at = [var 3, var 4])
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 mock_ctx = ExecutorContext::new_mock(None).await;
7737        let version = Version(0);
7738
7739        frontend.program = program.clone();
7740        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
7741        frontend.update_state_after_exec(outcome, true);
7742        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7743        let sketch_id = sketch_object.id;
7744        let sketch = expect_sketch(sketch_object);
7745        let segments = sketch
7746            .segments
7747            .iter()
7748            .take(3)
7749            .copied()
7750            .map(Into::into)
7751            .collect::<Vec<ConstraintSegment>>();
7752
7753        let constraint = Constraint::Coincident(Coincident {
7754            segments: segments.clone(),
7755        });
7756        let (src_delta, scene_delta) = frontend
7757            .add_constraint(&mock_ctx, version, sketch_id, constraint)
7758            .await
7759            .unwrap();
7760        assert_eq!(
7761            src_delta.text.as_str(),
7762            "\
7763sketch(on = XY) {
7764  point1 = point(at = [var 1, var 2])
7765  point2 = point(at = [var 3, var 4])
7766  point3 = point(at = [var 5, var 6])
7767  coincident([point1, point2, point3])
7768}
7769"
7770        );
7771
7772        let constraint_object = scene_delta
7773            .new_graph
7774            .objects
7775            .iter()
7776            .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
7777            .unwrap();
7778
7779        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
7780            panic!("expected a constraint object");
7781        };
7782
7783        assert_eq!(constraint, &Constraint::Coincident(Coincident { segments }));
7784
7785        mock_ctx.close().await;
7786    }
7787
7788    #[tokio::test(flavor = "multi_thread")]
7789    async fn test_source_with_three_point_coincident_tracks_all_segments() {
7790        let initial_source = "\
7791sketch(on = XY) {
7792  point1 = point(at = [var 1, var 2])
7793  point2 = point(at = [var 3, var 4])
7794  point3 = point(at = [var 5, var 6])
7795  coincident([point1, point2, point3])
7796}
7797";
7798
7799        let program = Program::parse(initial_source).unwrap().0.unwrap();
7800
7801        let mut frontend = FrontendState::new();
7802
7803        let ctx = ExecutorContext::new_mock(None).await;
7804        frontend.program = program.clone();
7805        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
7806        frontend.update_state_after_exec(outcome, true);
7807
7808        let constraint_object = frontend
7809            .scene_graph
7810            .objects
7811            .iter()
7812            .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
7813            .unwrap();
7814        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
7815            panic!("expected a constraint object");
7816        };
7817
7818        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7819        let sketch = expect_sketch(sketch_object);
7820        let expected_segments = sketch
7821            .segments
7822            .iter()
7823            .take(3)
7824            .copied()
7825            .map(Into::into)
7826            .collect::<Vec<ConstraintSegment>>();
7827
7828        assert_eq!(
7829            constraint,
7830            &Constraint::Coincident(Coincident {
7831                segments: expected_segments,
7832            })
7833        );
7834
7835        ctx.close().await;
7836    }
7837
7838    #[tokio::test(flavor = "multi_thread")]
7839    async fn test_point_origin_coincident_preserves_order() {
7840        let initial_source = "\
7841sketch(on = XY) {
7842  point(at = [var 1, var 2])
7843}
7844";
7845
7846        for (origin_first, expected_source) in [
7847            (
7848                true,
7849                "\
7850sketch(on = XY) {
7851  point1 = point(at = [var 1, var 2])
7852  coincident([ORIGIN, point1])
7853}
7854",
7855            ),
7856            (
7857                false,
7858                "\
7859sketch(on = XY) {
7860  point1 = point(at = [var 1, var 2])
7861  coincident([point1, ORIGIN])
7862}
7863",
7864            ),
7865        ] {
7866            let program = Program::parse(initial_source).unwrap().0.unwrap();
7867
7868            let mut frontend = FrontendState::new();
7869
7870            let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7871            let mock_ctx = ExecutorContext::new_mock(None).await;
7872            let version = Version(0);
7873
7874            frontend.hack_set_program(&ctx, program).await.unwrap();
7875            let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7876            let sketch_id = sketch_object.id;
7877            let sketch = expect_sketch(sketch_object);
7878            let point_id = *sketch.segments.first().unwrap();
7879
7880            let segments = if origin_first {
7881                vec![ConstraintSegment::ORIGIN, point_id.into()]
7882            } else {
7883                vec![point_id.into(), ConstraintSegment::ORIGIN]
7884            };
7885            let constraint = Constraint::Coincident(Coincident {
7886                segments: segments.clone(),
7887            });
7888            let (src_delta, scene_delta) = frontend
7889                .add_constraint(&mock_ctx, version, sketch_id, constraint)
7890                .await
7891                .unwrap();
7892            assert_eq!(src_delta.text.as_str(), expected_source);
7893
7894            let constraint_object = scene_delta
7895                .new_graph
7896                .objects
7897                .iter()
7898                .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
7899                .unwrap();
7900
7901            let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
7902                panic!("expected a constraint object");
7903            };
7904
7905            assert_eq!(constraint, &Constraint::Coincident(Coincident { segments }));
7906
7907            ctx.close().await;
7908            mock_ctx.close().await;
7909        }
7910    }
7911
7912    #[tokio::test(flavor = "multi_thread")]
7913    async fn test_coincident_of_line_end_points() {
7914        let initial_source = "\
7915sketch(on = XY) {
7916  line(start = [var 1, var 2], end = [var 3, var 4])
7917  line(start = [var 5, var 6], end = [var 7, var 8])
7918}
7919";
7920
7921        let program = Program::parse(initial_source).unwrap().0.unwrap();
7922
7923        let mut frontend = FrontendState::new();
7924
7925        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7926        let mock_ctx = ExecutorContext::new_mock(None).await;
7927        let version = Version(0);
7928
7929        frontend.hack_set_program(&ctx, program).await.unwrap();
7930        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7931        let sketch_id = sketch_object.id;
7932        let sketch = expect_sketch(sketch_object);
7933        let point0_id = *sketch.segments.get(1).unwrap();
7934        let point1_id = *sketch.segments.get(3).unwrap();
7935
7936        let constraint = Constraint::Coincident(Coincident {
7937            segments: vec![point0_id.into(), point1_id.into()],
7938        });
7939        let (src_delta, scene_delta) = frontend
7940            .add_constraint(&mock_ctx, version, sketch_id, constraint)
7941            .await
7942            .unwrap();
7943        assert_eq!(
7944            src_delta.text.as_str(),
7945            "\
7946sketch(on = XY) {
7947  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
7948  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
7949  coincident([line1.end, line2.start])
7950}
7951"
7952        );
7953        assert_eq!(
7954            scene_delta.new_graph.objects.len(),
7955            9,
7956            "{:#?}",
7957            scene_delta.new_graph.objects
7958        );
7959
7960        ctx.close().await;
7961        mock_ctx.close().await;
7962    }
7963
7964    #[tokio::test(flavor = "multi_thread")]
7965    async fn test_coincident_of_line_point_and_circle_segment() {
7966        let initial_source = "\
7967sketch(on = XY) {
7968  circle1 = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
7969  line1 = line(start = [var 9mm, var 1mm], end = [var 10mm, var 2mm])
7970}
7971";
7972        let program = Program::parse(initial_source).unwrap().0.unwrap();
7973        let mut frontend = FrontendState::new();
7974
7975        let mock_ctx = ExecutorContext::new_mock(None).await;
7976        let version = Version(0);
7977
7978        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
7979        frontend.program = program;
7980        frontend.update_state_after_exec(outcome, true);
7981        let sketch_object = find_first_sketch_object(&frontend.scene_graph).expect("Expected sketch object");
7982        let sketch_id = sketch_object.id;
7983        let sketch = expect_sketch(sketch_object);
7984
7985        let circle_id = sketch
7986            .segments
7987            .iter()
7988            .copied()
7989            .find(|seg_id| {
7990                matches!(
7991                    &frontend.scene_graph.objects[seg_id.0].kind,
7992                    ObjectKind::Segment {
7993                        segment: Segment::Circle(_)
7994                    }
7995                )
7996            })
7997            .expect("Expected a circle segment in sketch");
7998        let line_id = sketch
7999            .segments
8000            .iter()
8001            .copied()
8002            .find(|seg_id| {
8003                matches!(
8004                    &frontend.scene_graph.objects[seg_id.0].kind,
8005                    ObjectKind::Segment {
8006                        segment: Segment::Line(_)
8007                    }
8008                )
8009            })
8010            .expect("Expected a line segment in sketch");
8011
8012        let line_start_point_id = match &frontend.scene_graph.objects[line_id.0].kind {
8013            ObjectKind::Segment {
8014                segment: Segment::Line(line),
8015            } => line.start,
8016            _ => panic!("Expected line segment object"),
8017        };
8018
8019        let constraint = Constraint::Coincident(Coincident {
8020            segments: vec![line_start_point_id.into(), circle_id.into()],
8021        });
8022        let (src_delta, _scene_delta) = frontend
8023            .add_constraint(&mock_ctx, version, sketch_id, constraint)
8024            .await
8025            .unwrap();
8026        assert_eq!(
8027            src_delta.text.as_str(),
8028            "\
8029sketch(on = XY) {
8030  circle1 = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
8031  line1 = line(start = [var 9mm, var 1mm], end = [var 10mm, var 2mm])
8032  coincident([line1.start, circle1])
8033}
8034"
8035        );
8036
8037        mock_ctx.close().await;
8038    }
8039
8040    #[tokio::test(flavor = "multi_thread")]
8041    async fn test_invalid_coincident_arc_and_line_preserves_state() {
8042        // Test that attempting an invalid coincident constraint (arc and line)
8043        // doesn't corrupt the state, allowing subsequent operations to work.
8044        // This test verifies the transactional fix in add_constraint that prevents
8045        // state corruption when invalid constraints are attempted.
8046        // Example: coincident constraint between an arc segment and a straight line segment
8047        // is geometrically invalid and should fail, but state should remain intact.
8048        // Use the programmatic approach (new_sketch + add_segment) like test_new_sketch_add_arc_edit_arc
8049        let program = Program::empty();
8050
8051        let mut frontend = FrontendState::new();
8052        frontend.program = program;
8053
8054        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8055        let mock_ctx = ExecutorContext::new_mock(None).await;
8056        let version = Version(0);
8057
8058        let sketch_args = SketchCtor {
8059            on: Plane::Default(PlaneName::Xy),
8060        };
8061        let (_src_delta, _scene_delta, sketch_id) = frontend
8062            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
8063            .await
8064            .unwrap();
8065
8066        // Add an arc segment
8067        let arc_ctor = ArcCtor {
8068            start: Point2d {
8069                x: Expr::Var(Number {
8070                    value: 0.0,
8071                    units: NumericSuffix::Mm,
8072                }),
8073                y: Expr::Var(Number {
8074                    value: 0.0,
8075                    units: NumericSuffix::Mm,
8076                }),
8077            },
8078            end: Point2d {
8079                x: Expr::Var(Number {
8080                    value: 10.0,
8081                    units: NumericSuffix::Mm,
8082                }),
8083                y: Expr::Var(Number {
8084                    value: 10.0,
8085                    units: NumericSuffix::Mm,
8086                }),
8087            },
8088            center: Point2d {
8089                x: Expr::Var(Number {
8090                    value: 10.0,
8091                    units: NumericSuffix::Mm,
8092                }),
8093                y: Expr::Var(Number {
8094                    value: 0.0,
8095                    units: NumericSuffix::Mm,
8096                }),
8097            },
8098            construction: None,
8099        };
8100        let (_src_delta, scene_delta) = frontend
8101            .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Arc(arc_ctor), None)
8102            .await
8103            .unwrap();
8104        // The arc is the last object in new_objects (after the 3 points: start, end, center)
8105        let arc_id = *scene_delta.new_objects.last().unwrap();
8106
8107        // Add a line segment
8108        let line_ctor = LineCtor {
8109            start: Point2d {
8110                x: Expr::Var(Number {
8111                    value: 20.0,
8112                    units: NumericSuffix::Mm,
8113                }),
8114                y: Expr::Var(Number {
8115                    value: 0.0,
8116                    units: NumericSuffix::Mm,
8117                }),
8118            },
8119            end: Point2d {
8120                x: Expr::Var(Number {
8121                    value: 30.0,
8122                    units: NumericSuffix::Mm,
8123                }),
8124                y: Expr::Var(Number {
8125                    value: 10.0,
8126                    units: NumericSuffix::Mm,
8127                }),
8128            },
8129            construction: None,
8130        };
8131        let (_src_delta, scene_delta) = frontend
8132            .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Line(line_ctor), None)
8133            .await
8134            .unwrap();
8135        // The line is the last object in new_objects (after the 2 points: start, end)
8136        let line_id = *scene_delta.new_objects.last().unwrap();
8137
8138        // Attempt to add an invalid coincident constraint between arc and line
8139        // This should fail during execution, but state should remain intact
8140        let constraint = Constraint::Coincident(Coincident {
8141            segments: vec![arc_id.into(), line_id.into()],
8142        });
8143        let result = frontend.add_constraint(&mock_ctx, version, sketch_id, constraint).await;
8144
8145        // The constraint addition should fail (invalid constraint)
8146        assert!(result.is_err(), "Expected invalid coincident constraint to fail");
8147
8148        // Verify state is not corrupted by checking that we can still access the scene graph
8149        // and that the original segments are still present with their source ranges
8150        let sketch_object_after =
8151            find_first_sketch_object(&frontend.scene_graph).expect("Sketch should still exist after failed constraint");
8152        let sketch_after = expect_sketch(sketch_object_after);
8153
8154        // Verify both segments are still in the sketch
8155        assert!(
8156            sketch_after.segments.contains(&arc_id),
8157            "Arc segment should still exist after failed constraint"
8158        );
8159        assert!(
8160            sketch_after.segments.contains(&line_id),
8161            "Line segment should still exist after failed constraint"
8162        );
8163
8164        // Verify we can still access segment objects (this would fail if source ranges were corrupted)
8165        let arc_obj = frontend
8166            .scene_graph
8167            .objects
8168            .get(arc_id.0)
8169            .expect("Arc object should still be accessible");
8170        let line_obj = frontend
8171            .scene_graph
8172            .objects
8173            .get(line_id.0)
8174            .expect("Line object should still be accessible");
8175
8176        // Verify source ranges are still valid (not corrupted)
8177        // Just verify that the objects are still accessible and have the expected types
8178        match &arc_obj.kind {
8179            ObjectKind::Segment {
8180                segment: Segment::Arc(_),
8181            } => {}
8182            _ => panic!("Arc object should still be an arc segment"),
8183        }
8184        match &line_obj.kind {
8185            ObjectKind::Segment {
8186                segment: Segment::Line(_),
8187            } => {}
8188            _ => panic!("Line object should still be a line segment"),
8189        }
8190
8191        ctx.close().await;
8192        mock_ctx.close().await;
8193    }
8194
8195    #[tokio::test(flavor = "multi_thread")]
8196    async fn test_distance_two_points() {
8197        let initial_source = "\
8198sketch(on = XY) {
8199  point(at = [var 1, var 2])
8200  point(at = [var 3, var 4])
8201}
8202";
8203
8204        let program = Program::parse(initial_source).unwrap().0.unwrap();
8205
8206        let mut frontend = FrontendState::new();
8207
8208        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8209        let mock_ctx = ExecutorContext::new_mock(None).await;
8210        let version = Version(0);
8211
8212        frontend.hack_set_program(&ctx, program).await.unwrap();
8213        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8214        let sketch_id = sketch_object.id;
8215        let sketch = expect_sketch(sketch_object);
8216        let point0_id = *sketch.segments.first().unwrap();
8217        let point1_id = *sketch.segments.get(1).unwrap();
8218
8219        let constraint = Constraint::Distance(Distance {
8220            points: vec![point0_id.into(), point1_id.into()],
8221            distance: Number {
8222                value: 2.0,
8223                units: NumericSuffix::Mm,
8224            },
8225            source: Default::default(),
8226        });
8227        let (src_delta, scene_delta) = frontend
8228            .add_constraint(&mock_ctx, version, sketch_id, constraint)
8229            .await
8230            .unwrap();
8231        assert_eq!(
8232            src_delta.text.as_str(),
8233            // The lack indentation is a formatter bug.
8234            "\
8235sketch(on = XY) {
8236  point1 = point(at = [var 1, var 2])
8237  point2 = point(at = [var 3, var 4])
8238  distance([point1, point2]) == 2mm
8239}
8240"
8241        );
8242        assert_eq!(
8243            scene_delta.new_graph.objects.len(),
8244            5,
8245            "{:#?}",
8246            scene_delta.new_graph.objects
8247        );
8248
8249        ctx.close().await;
8250        mock_ctx.close().await;
8251    }
8252
8253    #[tokio::test(flavor = "multi_thread")]
8254    async fn test_horizontal_distance_two_points() {
8255        let initial_source = "\
8256sketch(on = XY) {
8257  point(at = [var 1, var 2])
8258  point(at = [var 3, var 4])
8259}
8260";
8261
8262        let program = Program::parse(initial_source).unwrap().0.unwrap();
8263
8264        let mut frontend = FrontendState::new();
8265
8266        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8267        let mock_ctx = ExecutorContext::new_mock(None).await;
8268        let version = Version(0);
8269
8270        frontend.hack_set_program(&ctx, program).await.unwrap();
8271        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8272        let sketch_id = sketch_object.id;
8273        let sketch = expect_sketch(sketch_object);
8274        let point0_id = *sketch.segments.first().unwrap();
8275        let point1_id = *sketch.segments.get(1).unwrap();
8276
8277        let constraint = Constraint::HorizontalDistance(Distance {
8278            points: vec![point0_id.into(), point1_id.into()],
8279            distance: Number {
8280                value: 2.0,
8281                units: NumericSuffix::Mm,
8282            },
8283            source: Default::default(),
8284        });
8285        let (src_delta, scene_delta) = frontend
8286            .add_constraint(&mock_ctx, version, sketch_id, constraint)
8287            .await
8288            .unwrap();
8289        assert_eq!(
8290            src_delta.text.as_str(),
8291            // The lack indentation is a formatter bug.
8292            "\
8293sketch(on = XY) {
8294  point1 = point(at = [var 1, var 2])
8295  point2 = point(at = [var 3, var 4])
8296  horizontalDistance([point1, point2]) == 2mm
8297}
8298"
8299        );
8300        assert_eq!(
8301            scene_delta.new_graph.objects.len(),
8302            5,
8303            "{:#?}",
8304            scene_delta.new_graph.objects
8305        );
8306
8307        ctx.close().await;
8308        mock_ctx.close().await;
8309    }
8310
8311    #[tokio::test(flavor = "multi_thread")]
8312    async fn test_radius_single_arc_segment() {
8313        let initial_source = "\
8314sketch(on = XY) {
8315  arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
8316}
8317";
8318
8319        let program = Program::parse(initial_source).unwrap().0.unwrap();
8320
8321        let mut frontend = FrontendState::new();
8322
8323        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8324        let mock_ctx = ExecutorContext::new_mock(None).await;
8325        let version = Version(0);
8326
8327        frontend.hack_set_program(&ctx, program).await.unwrap();
8328        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8329        let sketch_id = sketch_object.id;
8330        let sketch = expect_sketch(sketch_object);
8331        // Find the arc segment (not the points)
8332        let arc_id = sketch
8333            .segments
8334            .iter()
8335            .find(|&seg_id| {
8336                let obj = frontend.scene_graph.objects.get(seg_id.0);
8337                matches!(
8338                    obj.map(|o| &o.kind),
8339                    Some(ObjectKind::Segment {
8340                        segment: Segment::Arc(_)
8341                    })
8342                )
8343            })
8344            .unwrap();
8345
8346        let constraint = Constraint::Radius(Radius {
8347            arc: *arc_id,
8348            radius: Number {
8349                value: 5.0,
8350                units: NumericSuffix::Mm,
8351            },
8352            source: Default::default(),
8353        });
8354        let (src_delta, scene_delta) = frontend
8355            .add_constraint(&mock_ctx, version, sketch_id, constraint)
8356            .await
8357            .unwrap();
8358        assert_eq!(
8359            src_delta.text.as_str(),
8360            // The lack indentation is a formatter bug.
8361            "\
8362sketch(on = XY) {
8363  arc1 = arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
8364  radius(arc1) == 5mm
8365}
8366"
8367        );
8368        assert_eq!(
8369            scene_delta.new_graph.objects.len(),
8370            7, // Plane (0) + Sketch (1) + Start point (2) + End point (3) + Center point (4) + Arc (5) + Constraint (6)
8371            "{:#?}",
8372            scene_delta.new_graph.objects
8373        );
8374
8375        ctx.close().await;
8376        mock_ctx.close().await;
8377    }
8378
8379    #[tokio::test(flavor = "multi_thread")]
8380    async fn test_vertical_distance_two_points() {
8381        let initial_source = "\
8382sketch(on = XY) {
8383  point(at = [var 1, var 2])
8384  point(at = [var 3, var 4])
8385}
8386";
8387
8388        let program = Program::parse(initial_source).unwrap().0.unwrap();
8389
8390        let mut frontend = FrontendState::new();
8391
8392        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8393        let mock_ctx = ExecutorContext::new_mock(None).await;
8394        let version = Version(0);
8395
8396        frontend.hack_set_program(&ctx, program).await.unwrap();
8397        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8398        let sketch_id = sketch_object.id;
8399        let sketch = expect_sketch(sketch_object);
8400        let point0_id = *sketch.segments.first().unwrap();
8401        let point1_id = *sketch.segments.get(1).unwrap();
8402
8403        let constraint = Constraint::VerticalDistance(Distance {
8404            points: vec![point0_id.into(), point1_id.into()],
8405            distance: Number {
8406                value: 2.0,
8407                units: NumericSuffix::Mm,
8408            },
8409            source: Default::default(),
8410        });
8411        let (src_delta, scene_delta) = frontend
8412            .add_constraint(&mock_ctx, version, sketch_id, constraint)
8413            .await
8414            .unwrap();
8415        assert_eq!(
8416            src_delta.text.as_str(),
8417            // The lack indentation is a formatter bug.
8418            "\
8419sketch(on = XY) {
8420  point1 = point(at = [var 1, var 2])
8421  point2 = point(at = [var 3, var 4])
8422  verticalDistance([point1, point2]) == 2mm
8423}
8424"
8425        );
8426        assert_eq!(
8427            scene_delta.new_graph.objects.len(),
8428            5,
8429            "{:#?}",
8430            scene_delta.new_graph.objects
8431        );
8432
8433        ctx.close().await;
8434        mock_ctx.close().await;
8435    }
8436
8437    #[tokio::test(flavor = "multi_thread")]
8438    async fn test_add_fixed_standalone_point() {
8439        let initial_source = "\
8440sketch(on = XY) {
8441  point(at = [var 1, var 2])
8442}
8443";
8444
8445        let program = Program::parse(initial_source).unwrap().0.unwrap();
8446
8447        let mut frontend = FrontendState::new();
8448
8449        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8450        let mock_ctx = ExecutorContext::new_mock(None).await;
8451        let version = Version(0);
8452
8453        frontend.hack_set_program(&ctx, program).await.unwrap();
8454        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8455        let sketch_id = sketch_object.id;
8456        let sketch = expect_sketch(sketch_object);
8457        let point_id = *sketch.segments.first().unwrap();
8458
8459        let (src_delta, scene_delta) = frontend
8460            .add_constraint(
8461                &mock_ctx,
8462                version,
8463                sketch_id,
8464                Constraint::Fixed(Fixed {
8465                    points: vec![FixedPoint {
8466                        point: point_id,
8467                        position: Point2d {
8468                            x: Number {
8469                                value: 2.0,
8470                                units: NumericSuffix::Mm,
8471                            },
8472                            y: Number {
8473                                value: 3.0,
8474                                units: NumericSuffix::Mm,
8475                            },
8476                        },
8477                    }],
8478                }),
8479            )
8480            .await
8481            .unwrap();
8482        assert_eq!(
8483            src_delta.text.as_str(),
8484            "\
8485sketch(on = XY) {
8486  point1 = point(at = [var 1, var 2])
8487  fixed([point1, [2mm, 3mm]])
8488}
8489"
8490        );
8491        assert_eq!(
8492            scene_delta.new_graph.objects.len(),
8493            4,
8494            "{:#?}",
8495            scene_delta.new_graph.objects
8496        );
8497
8498        ctx.close().await;
8499        mock_ctx.close().await;
8500    }
8501
8502    #[tokio::test(flavor = "multi_thread")]
8503    async fn test_add_fixed_multiple_points() {
8504        let initial_source = "\
8505sketch(on = XY) {
8506  point(at = [var 1, var 2])
8507  point(at = [var 3, var 4])
8508}
8509";
8510
8511        let program = Program::parse(initial_source).unwrap().0.unwrap();
8512
8513        let mut frontend = FrontendState::new();
8514
8515        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8516        let mock_ctx = ExecutorContext::new_mock(None).await;
8517        let version = Version(0);
8518
8519        frontend.hack_set_program(&ctx, program).await.unwrap();
8520        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8521        let sketch_id = sketch_object.id;
8522        let sketch = expect_sketch(sketch_object);
8523        let point0_id = *sketch.segments.first().unwrap();
8524        let point1_id = *sketch.segments.get(1).unwrap();
8525
8526        let (src_delta, scene_delta) = frontend
8527            .add_constraint(
8528                &mock_ctx,
8529                version,
8530                sketch_id,
8531                Constraint::Fixed(Fixed {
8532                    points: vec![
8533                        FixedPoint {
8534                            point: point0_id,
8535                            position: Point2d {
8536                                x: Number {
8537                                    value: 2.0,
8538                                    units: NumericSuffix::Mm,
8539                                },
8540                                y: Number {
8541                                    value: 3.0,
8542                                    units: NumericSuffix::Mm,
8543                                },
8544                            },
8545                        },
8546                        FixedPoint {
8547                            point: point1_id,
8548                            position: Point2d {
8549                                x: Number {
8550                                    value: 4.0,
8551                                    units: NumericSuffix::Mm,
8552                                },
8553                                y: Number {
8554                                    value: 5.0,
8555                                    units: NumericSuffix::Mm,
8556                                },
8557                            },
8558                        },
8559                    ],
8560                }),
8561            )
8562            .await
8563            .unwrap();
8564        assert_eq!(
8565            src_delta.text.as_str(),
8566            "\
8567sketch(on = XY) {
8568  point1 = point(at = [var 1, var 2])
8569  point2 = point(at = [var 3, var 4])
8570  fixed([point1, [2mm, 3mm]])
8571  fixed([point2, [4mm, 5mm]])
8572}
8573"
8574        );
8575        assert_eq!(
8576            scene_delta.new_graph.objects.len(),
8577            6,
8578            "{:#?}",
8579            scene_delta.new_graph.objects
8580        );
8581
8582        ctx.close().await;
8583        mock_ctx.close().await;
8584    }
8585
8586    #[tokio::test(flavor = "multi_thread")]
8587    async fn test_add_fixed_owned_point() {
8588        let initial_source = "\
8589sketch(on = XY) {
8590  line(start = [var 1, var 2], end = [var 3, var 4])
8591}
8592";
8593
8594        let program = Program::parse(initial_source).unwrap().0.unwrap();
8595
8596        let mut frontend = FrontendState::new();
8597
8598        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8599        let mock_ctx = ExecutorContext::new_mock(None).await;
8600        let version = Version(0);
8601
8602        frontend.hack_set_program(&ctx, program).await.unwrap();
8603        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8604        let sketch_id = sketch_object.id;
8605        let sketch = expect_sketch(sketch_object);
8606        let line_start_id = *sketch.segments.first().unwrap();
8607
8608        let (src_delta, scene_delta) = frontend
8609            .add_constraint(
8610                &mock_ctx,
8611                version,
8612                sketch_id,
8613                Constraint::Fixed(Fixed {
8614                    points: vec![FixedPoint {
8615                        point: line_start_id,
8616                        position: Point2d {
8617                            x: Number {
8618                                value: 2.0,
8619                                units: NumericSuffix::Mm,
8620                            },
8621                            y: Number {
8622                                value: 3.0,
8623                                units: NumericSuffix::Mm,
8624                            },
8625                        },
8626                    }],
8627                }),
8628            )
8629            .await
8630            .unwrap();
8631        assert_eq!(
8632            src_delta.text.as_str(),
8633            "\
8634sketch(on = XY) {
8635  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8636  fixed([line1.start, [2mm, 3mm]])
8637}
8638"
8639        );
8640        assert_eq!(
8641            scene_delta.new_graph.objects.len(),
8642            6,
8643            "{:#?}",
8644            scene_delta.new_graph.objects
8645        );
8646
8647        ctx.close().await;
8648        mock_ctx.close().await;
8649    }
8650
8651    #[tokio::test(flavor = "multi_thread")]
8652    async fn test_radius_error_cases() {
8653        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8654        let mock_ctx = ExecutorContext::new_mock(None).await;
8655        let version = Version(0);
8656
8657        // Test: Single point should error
8658        let initial_source_point = "\
8659sketch(on = XY) {
8660  point(at = [var 1, var 2])
8661}
8662";
8663        let program_point = Program::parse(initial_source_point).unwrap().0.unwrap();
8664        let mut frontend_point = FrontendState::new();
8665        frontend_point.hack_set_program(&ctx, program_point).await.unwrap();
8666        let sketch_object_point = find_first_sketch_object(&frontend_point.scene_graph).unwrap();
8667        let sketch_id_point = sketch_object_point.id;
8668        let sketch_point = expect_sketch(sketch_object_point);
8669        let point_id = *sketch_point.segments.first().unwrap();
8670
8671        let constraint_point = Constraint::Radius(Radius {
8672            arc: point_id,
8673            radius: Number {
8674                value: 5.0,
8675                units: NumericSuffix::Mm,
8676            },
8677            source: Default::default(),
8678        });
8679        let result_point = frontend_point
8680            .add_constraint(&mock_ctx, version, sketch_id_point, constraint_point)
8681            .await;
8682        assert!(result_point.is_err(), "Single point should error for radius");
8683
8684        // Test: Single line segment should error (only arc segments supported)
8685        let initial_source_line = "\
8686sketch(on = XY) {
8687  line(start = [var 1, var 2], end = [var 3, var 4])
8688}
8689";
8690        let program_line = Program::parse(initial_source_line).unwrap().0.unwrap();
8691        let mut frontend_line = FrontendState::new();
8692        frontend_line.hack_set_program(&ctx, program_line).await.unwrap();
8693        let sketch_object_line = find_first_sketch_object(&frontend_line.scene_graph).unwrap();
8694        let sketch_id_line = sketch_object_line.id;
8695        let sketch_line = expect_sketch(sketch_object_line);
8696        let line_id = *sketch_line.segments.first().unwrap();
8697
8698        let constraint_line = Constraint::Radius(Radius {
8699            arc: line_id,
8700            radius: Number {
8701                value: 5.0,
8702                units: NumericSuffix::Mm,
8703            },
8704            source: Default::default(),
8705        });
8706        let result_line = frontend_line
8707            .add_constraint(&mock_ctx, version, sketch_id_line, constraint_line)
8708            .await;
8709        assert!(result_line.is_err(), "Single line segment should error for radius");
8710
8711        ctx.close().await;
8712        mock_ctx.close().await;
8713    }
8714
8715    #[tokio::test(flavor = "multi_thread")]
8716    async fn test_diameter_single_arc_segment() {
8717        let initial_source = "\
8718sketch(on = XY) {
8719  arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
8720}
8721";
8722
8723        let program = Program::parse(initial_source).unwrap().0.unwrap();
8724
8725        let mut frontend = FrontendState::new();
8726
8727        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8728        let mock_ctx = ExecutorContext::new_mock(None).await;
8729        let version = Version(0);
8730
8731        frontend.hack_set_program(&ctx, program).await.unwrap();
8732        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8733        let sketch_id = sketch_object.id;
8734        let sketch = expect_sketch(sketch_object);
8735        // Find the arc segment (not the points)
8736        let arc_id = sketch
8737            .segments
8738            .iter()
8739            .find(|&seg_id| {
8740                let obj = frontend.scene_graph.objects.get(seg_id.0);
8741                matches!(
8742                    obj.map(|o| &o.kind),
8743                    Some(ObjectKind::Segment {
8744                        segment: Segment::Arc(_)
8745                    })
8746                )
8747            })
8748            .unwrap();
8749
8750        let constraint = Constraint::Diameter(Diameter {
8751            arc: *arc_id,
8752            diameter: Number {
8753                value: 10.0,
8754                units: NumericSuffix::Mm,
8755            },
8756            source: Default::default(),
8757        });
8758        let (src_delta, scene_delta) = frontend
8759            .add_constraint(&mock_ctx, version, sketch_id, constraint)
8760            .await
8761            .unwrap();
8762        assert_eq!(
8763            src_delta.text.as_str(),
8764            // The lack indentation is a formatter bug.
8765            "\
8766sketch(on = XY) {
8767  arc1 = arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
8768  diameter(arc1) == 10mm
8769}
8770"
8771        );
8772        assert_eq!(
8773            scene_delta.new_graph.objects.len(),
8774            7, // Plane (0) + Sketch (1) + Start point (2) + End point (3) + Center point (4) + Arc (5) + Constraint (6)
8775            "{:#?}",
8776            scene_delta.new_graph.objects
8777        );
8778
8779        ctx.close().await;
8780        mock_ctx.close().await;
8781    }
8782
8783    #[tokio::test(flavor = "multi_thread")]
8784    async fn test_diameter_error_cases() {
8785        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8786        let mock_ctx = ExecutorContext::new_mock(None).await;
8787        let version = Version(0);
8788
8789        // Test: Single point should error
8790        let initial_source_point = "\
8791sketch(on = XY) {
8792  point(at = [var 1, var 2])
8793}
8794";
8795        let program_point = Program::parse(initial_source_point).unwrap().0.unwrap();
8796        let mut frontend_point = FrontendState::new();
8797        frontend_point.hack_set_program(&ctx, program_point).await.unwrap();
8798        let sketch_object_point = find_first_sketch_object(&frontend_point.scene_graph).unwrap();
8799        let sketch_id_point = sketch_object_point.id;
8800        let sketch_point = expect_sketch(sketch_object_point);
8801        let point_id = *sketch_point.segments.first().unwrap();
8802
8803        let constraint_point = Constraint::Diameter(Diameter {
8804            arc: point_id,
8805            diameter: Number {
8806                value: 10.0,
8807                units: NumericSuffix::Mm,
8808            },
8809            source: Default::default(),
8810        });
8811        let result_point = frontend_point
8812            .add_constraint(&mock_ctx, version, sketch_id_point, constraint_point)
8813            .await;
8814        assert!(result_point.is_err(), "Single point should error for diameter");
8815
8816        // Test: Single line segment should error (only arc segments supported)
8817        let initial_source_line = "\
8818sketch(on = XY) {
8819  line(start = [var 1, var 2], end = [var 3, var 4])
8820}
8821";
8822        let program_line = Program::parse(initial_source_line).unwrap().0.unwrap();
8823        let mut frontend_line = FrontendState::new();
8824        frontend_line.hack_set_program(&ctx, program_line).await.unwrap();
8825        let sketch_object_line = find_first_sketch_object(&frontend_line.scene_graph).unwrap();
8826        let sketch_id_line = sketch_object_line.id;
8827        let sketch_line = expect_sketch(sketch_object_line);
8828        let line_id = *sketch_line.segments.first().unwrap();
8829
8830        let constraint_line = Constraint::Diameter(Diameter {
8831            arc: line_id,
8832            diameter: Number {
8833                value: 10.0,
8834                units: NumericSuffix::Mm,
8835            },
8836            source: Default::default(),
8837        });
8838        let result_line = frontend_line
8839            .add_constraint(&mock_ctx, version, sketch_id_line, constraint_line)
8840            .await;
8841        assert!(result_line.is_err(), "Single line segment should error for diameter");
8842
8843        ctx.close().await;
8844        mock_ctx.close().await;
8845    }
8846
8847    #[tokio::test(flavor = "multi_thread")]
8848    async fn test_line_horizontal() {
8849        let initial_source = "\
8850sketch(on = XY) {
8851  line(start = [var 1, var 2], end = [var 3, var 4])
8852}
8853";
8854
8855        let program = Program::parse(initial_source).unwrap().0.unwrap();
8856
8857        let mut frontend = FrontendState::new();
8858
8859        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8860        let mock_ctx = ExecutorContext::new_mock(None).await;
8861        let version = Version(0);
8862
8863        frontend.hack_set_program(&ctx, program).await.unwrap();
8864        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8865        let sketch_id = sketch_object.id;
8866        let sketch = expect_sketch(sketch_object);
8867        let line1_id = *sketch.segments.get(2).unwrap();
8868
8869        let constraint = Constraint::Horizontal(Horizontal::Line { line: line1_id });
8870        let (src_delta, scene_delta) = frontend
8871            .add_constraint(&mock_ctx, version, sketch_id, constraint)
8872            .await
8873            .unwrap();
8874        assert_eq!(
8875            src_delta.text.as_str(),
8876            "\
8877sketch(on = XY) {
8878  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8879  horizontal(line1)
8880}
8881"
8882        );
8883        assert_eq!(
8884            scene_delta.new_graph.objects.len(),
8885            6,
8886            "{:#?}",
8887            scene_delta.new_graph.objects
8888        );
8889
8890        ctx.close().await;
8891        mock_ctx.close().await;
8892    }
8893
8894    #[tokio::test(flavor = "multi_thread")]
8895    async fn test_line_vertical() {
8896        let initial_source = "\
8897sketch(on = XY) {
8898  line(start = [var 1, var 2], end = [var 3, var 4])
8899}
8900";
8901
8902        let program = Program::parse(initial_source).unwrap().0.unwrap();
8903
8904        let mut frontend = FrontendState::new();
8905
8906        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8907        let mock_ctx = ExecutorContext::new_mock(None).await;
8908        let version = Version(0);
8909
8910        frontend.hack_set_program(&ctx, program).await.unwrap();
8911        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8912        let sketch_id = sketch_object.id;
8913        let sketch = expect_sketch(sketch_object);
8914        let line1_id = *sketch.segments.get(2).unwrap();
8915
8916        let constraint = Constraint::Vertical(Vertical::Line { line: line1_id });
8917        let (src_delta, scene_delta) = frontend
8918            .add_constraint(&mock_ctx, version, sketch_id, constraint)
8919            .await
8920            .unwrap();
8921        assert_eq!(
8922            src_delta.text.as_str(),
8923            "\
8924sketch(on = XY) {
8925  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8926  vertical(line1)
8927}
8928"
8929        );
8930        assert_eq!(
8931            scene_delta.new_graph.objects.len(),
8932            6,
8933            "{:#?}",
8934            scene_delta.new_graph.objects
8935        );
8936
8937        ctx.close().await;
8938        mock_ctx.close().await;
8939    }
8940
8941    #[tokio::test(flavor = "multi_thread")]
8942    async fn test_points_vertical() {
8943        let initial_source = "\
8944sketch001 = sketch(on = XY) {
8945  p0 = point(at = [var -2.23mm, var 3.1mm])
8946  pf = point(at = [4, 4])
8947}
8948";
8949
8950        let program = Program::parse(initial_source).unwrap().0.unwrap();
8951
8952        let mut frontend = FrontendState::new();
8953
8954        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8955        let mock_ctx = ExecutorContext::new_mock(None).await;
8956        let version = Version(0);
8957
8958        frontend.hack_set_program(&ctx, program).await.unwrap();
8959        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8960        let sketch_id = sketch_object.id;
8961        let sketch = expect_sketch(sketch_object);
8962        let point_ids = vec![
8963            sketch.segments.first().unwrap().to_owned(),
8964            sketch.segments.get(1).unwrap().to_owned(),
8965        ];
8966
8967        let constraint = Constraint::Vertical(Vertical::Points {
8968            points: point_ids.into_iter().map(ConstraintSegment::from).collect(),
8969        });
8970        let (src_delta, scene_delta) = frontend
8971            .add_constraint(&mock_ctx, version, sketch_id, constraint)
8972            .await
8973            .unwrap();
8974        assert_eq!(
8975            src_delta.text.as_str(),
8976            "\
8977sketch001 = sketch(on = XY) {
8978  p0 = point(at = [var -2.23mm, var 3.1mm])
8979  pf = point(at = [4, 4])
8980  vertical([p0, pf])
8981}
8982"
8983        );
8984        assert_eq!(
8985            scene_delta.new_graph.objects.len(),
8986            5,
8987            "{:#?}",
8988            scene_delta.new_graph.objects
8989        );
8990
8991        ctx.close().await;
8992        mock_ctx.close().await;
8993    }
8994
8995    #[tokio::test(flavor = "multi_thread")]
8996    async fn test_points_horizontal() {
8997        let initial_source = "\
8998sketch001 = sketch(on = XY) {
8999  p0 = point(at = [var -2.23mm, var 3.1mm])
9000  pf = point(at = [4, 4])
9001}
9002";
9003
9004        let program = Program::parse(initial_source).unwrap().0.unwrap();
9005
9006        let mut frontend = FrontendState::new();
9007
9008        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9009        let mock_ctx = ExecutorContext::new_mock(None).await;
9010        let version = Version(0);
9011
9012        frontend.hack_set_program(&ctx, program).await.unwrap();
9013        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9014        let sketch_id = sketch_object.id;
9015        let sketch = expect_sketch(sketch_object);
9016        let point_ids = vec![
9017            sketch.segments.first().unwrap().to_owned(),
9018            sketch.segments.get(1).unwrap().to_owned(),
9019        ];
9020
9021        let constraint = Constraint::Horizontal(Horizontal::Points {
9022            points: point_ids.into_iter().map(ConstraintSegment::from).collect(),
9023        });
9024        let (src_delta, scene_delta) = frontend
9025            .add_constraint(&mock_ctx, version, sketch_id, constraint)
9026            .await
9027            .unwrap();
9028        assert_eq!(
9029            src_delta.text.as_str(),
9030            "\
9031sketch001 = sketch(on = XY) {
9032  p0 = point(at = [var -2.23mm, var 3.1mm])
9033  pf = point(at = [4, 4])
9034  horizontal([p0, pf])
9035}
9036"
9037        );
9038        assert_eq!(
9039            scene_delta.new_graph.objects.len(),
9040            5,
9041            "{:#?}",
9042            scene_delta.new_graph.objects
9043        );
9044
9045        ctx.close().await;
9046        mock_ctx.close().await;
9047    }
9048
9049    #[tokio::test(flavor = "multi_thread")]
9050    async fn test_point_horizontal_with_origin() {
9051        let initial_source = "\
9052sketch001 = sketch(on = XY) {
9053  p0 = point(at = [var -2.23mm, var 3.1mm])
9054}
9055";
9056
9057        let program = Program::parse(initial_source).unwrap().0.unwrap();
9058
9059        let mut frontend = FrontendState::new();
9060
9061        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9062        let mock_ctx = ExecutorContext::new_mock(None).await;
9063        let version = Version(0);
9064
9065        frontend.hack_set_program(&ctx, program).await.unwrap();
9066        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9067        let sketch_id = sketch_object.id;
9068        let sketch = expect_sketch(sketch_object);
9069        let point_id = *sketch.segments.first().unwrap();
9070
9071        let constraint = Constraint::Horizontal(Horizontal::Points {
9072            points: vec![ConstraintSegment::from(point_id), ConstraintSegment::ORIGIN],
9073        });
9074        let (src_delta, scene_delta) = frontend
9075            .add_constraint(&mock_ctx, version, sketch_id, constraint)
9076            .await
9077            .unwrap();
9078        assert_eq!(
9079            src_delta.text.as_str(),
9080            "\
9081sketch001 = sketch(on = XY) {
9082  p0 = point(at = [var -2.23mm, var 3.1mm])
9083  horizontal([p0, ORIGIN])
9084}
9085"
9086        );
9087        assert_eq!(
9088            scene_delta.new_graph.objects.len(),
9089            4,
9090            "{:#?}",
9091            scene_delta.new_graph.objects
9092        );
9093
9094        ctx.close().await;
9095        mock_ctx.close().await;
9096    }
9097
9098    #[tokio::test(flavor = "multi_thread")]
9099    async fn test_lines_equal_length() {
9100        let initial_source = "\
9101sketch(on = XY) {
9102  line(start = [var 1, var 2], end = [var 3, var 4])
9103  line(start = [var 5, var 6], end = [var 7, var 8])
9104}
9105";
9106
9107        let program = Program::parse(initial_source).unwrap().0.unwrap();
9108
9109        let mut frontend = FrontendState::new();
9110
9111        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9112        let mock_ctx = ExecutorContext::new_mock(None).await;
9113        let version = Version(0);
9114
9115        frontend.hack_set_program(&ctx, program).await.unwrap();
9116        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9117        let sketch_id = sketch_object.id;
9118        let sketch = expect_sketch(sketch_object);
9119        let line1_id = *sketch.segments.get(2).unwrap();
9120        let line2_id = *sketch.segments.get(5).unwrap();
9121
9122        let constraint = Constraint::LinesEqualLength(LinesEqualLength {
9123            lines: vec![line1_id, line2_id],
9124        });
9125        let (src_delta, scene_delta) = frontend
9126            .add_constraint(&mock_ctx, version, sketch_id, constraint)
9127            .await
9128            .unwrap();
9129        assert_eq!(
9130            src_delta.text.as_str(),
9131            "\
9132sketch(on = XY) {
9133  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9134  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9135  equalLength([line1, line2])
9136}
9137"
9138        );
9139        assert_eq!(
9140            scene_delta.new_graph.objects.len(),
9141            9,
9142            "{:#?}",
9143            scene_delta.new_graph.objects
9144        );
9145
9146        ctx.close().await;
9147        mock_ctx.close().await;
9148    }
9149
9150    #[tokio::test(flavor = "multi_thread")]
9151    async fn test_add_constraint_multi_line_equal_length() {
9152        let initial_source = "\
9153sketch(on = XY) {
9154  line(start = [var 1, var 2], end = [var 3, var 4])
9155  line(start = [var 5, var 6], end = [var 7, var 8])
9156  line(start = [var 9, var 10], end = [var 11, var 12])
9157}
9158";
9159
9160        let program = Program::parse(initial_source).unwrap().0.unwrap();
9161
9162        let mut frontend = FrontendState::new();
9163        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9164        let mock_ctx = ExecutorContext::new_mock(None).await;
9165        let version = Version(0);
9166
9167        frontend.hack_set_program(&ctx, program).await.unwrap();
9168        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9169        let sketch_id = sketch_object.id;
9170        let sketch = expect_sketch(sketch_object);
9171        let line1_id = *sketch.segments.get(2).unwrap();
9172        let line2_id = *sketch.segments.get(5).unwrap();
9173        let line3_id = *sketch.segments.get(8).unwrap();
9174
9175        let constraint = Constraint::LinesEqualLength(LinesEqualLength {
9176            lines: vec![line1_id, line2_id, line3_id],
9177        });
9178        let (src_delta, scene_delta) = frontend
9179            .add_constraint(&mock_ctx, version, sketch_id, constraint)
9180            .await
9181            .unwrap();
9182        assert_eq!(
9183            src_delta.text.as_str(),
9184            "\
9185sketch(on = XY) {
9186  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9187  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9188  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
9189  equalLength([line1, line2, line3])
9190}
9191"
9192        );
9193        let constraints = scene_delta
9194            .new_graph
9195            .objects
9196            .iter()
9197            .filter_map(|obj| {
9198                let ObjectKind::Constraint { constraint } = &obj.kind else {
9199                    return None;
9200                };
9201                Some(constraint)
9202            })
9203            .collect::<Vec<_>>();
9204
9205        assert_eq!(constraints.len(), 1, "{:#?}", frontend.scene_graph.objects);
9206        let Constraint::LinesEqualLength(lines_equal_length) = constraints[0] else {
9207            panic!("expected equal length constraint, got {:?}", constraints[0]);
9208        };
9209        assert_eq!(lines_equal_length.lines.len(), 3);
9210
9211        ctx.close().await;
9212        mock_ctx.close().await;
9213    }
9214
9215    #[tokio::test(flavor = "multi_thread")]
9216    async fn test_lines_parallel() {
9217        let initial_source = "\
9218sketch(on = XY) {
9219  line(start = [var 1, var 2], end = [var 3, var 4])
9220  line(start = [var 5, var 6], end = [var 7, var 8])
9221}
9222";
9223
9224        let program = Program::parse(initial_source).unwrap().0.unwrap();
9225
9226        let mut frontend = FrontendState::new();
9227
9228        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9229        let mock_ctx = ExecutorContext::new_mock(None).await;
9230        let version = Version(0);
9231
9232        frontend.hack_set_program(&ctx, program).await.unwrap();
9233        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9234        let sketch_id = sketch_object.id;
9235        let sketch = expect_sketch(sketch_object);
9236        let line1_id = *sketch.segments.get(2).unwrap();
9237        let line2_id = *sketch.segments.get(5).unwrap();
9238
9239        let constraint = Constraint::Parallel(Parallel {
9240            lines: vec![line1_id, line2_id],
9241        });
9242        let (src_delta, scene_delta) = frontend
9243            .add_constraint(&mock_ctx, version, sketch_id, constraint)
9244            .await
9245            .unwrap();
9246        assert_eq!(
9247            src_delta.text.as_str(),
9248            "\
9249sketch(on = XY) {
9250  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9251  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9252  parallel([line1, line2])
9253}
9254"
9255        );
9256        assert_eq!(
9257            scene_delta.new_graph.objects.len(),
9258            9,
9259            "{:#?}",
9260            scene_delta.new_graph.objects
9261        );
9262
9263        ctx.close().await;
9264        mock_ctx.close().await;
9265    }
9266
9267    #[tokio::test(flavor = "multi_thread")]
9268    async fn test_lines_parallel_multiline() {
9269        let initial_source = "\
9270sketch(on = XY) {
9271  line(start = [var 1, var 2], end = [var 3, var 4])
9272  line(start = [var 5, var 6], end = [var 7, var 8])
9273  line(start = [var 9, var 10], end = [var 11, var 12])
9274}
9275";
9276
9277        let program = Program::parse(initial_source).unwrap().0.unwrap();
9278
9279        let mut frontend = FrontendState::new();
9280
9281        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9282        let mock_ctx = ExecutorContext::new_mock(None).await;
9283        let version = Version(0);
9284
9285        frontend.hack_set_program(&ctx, program).await.unwrap();
9286        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9287        let sketch_id = sketch_object.id;
9288        let sketch = expect_sketch(sketch_object);
9289        let line1_id = *sketch.segments.get(2).unwrap();
9290        let line2_id = *sketch.segments.get(5).unwrap();
9291        let line3_id = *sketch.segments.get(8).unwrap();
9292
9293        let constraint = Constraint::Parallel(Parallel {
9294            lines: vec![line1_id, line2_id, line3_id],
9295        });
9296        let (src_delta, scene_delta) = frontend
9297            .add_constraint(&mock_ctx, version, sketch_id, constraint)
9298            .await
9299            .unwrap();
9300        assert_eq!(
9301            src_delta.text.as_str(),
9302            "\
9303sketch(on = XY) {
9304  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9305  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9306  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
9307  parallel([line1, line2, line3])
9308}
9309"
9310        );
9311
9312        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9313        let sketch = expect_sketch(sketch_object);
9314        assert_eq!(sketch.constraints.len(), 1);
9315
9316        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
9317        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9318            panic!("Expected constraint object");
9319        };
9320        let Constraint::Parallel(parallel) = constraint else {
9321            panic!("Expected parallel constraint");
9322        };
9323        assert_eq!(parallel.lines.len(), 3);
9324
9325        ctx.close().await;
9326        mock_ctx.close().await;
9327    }
9328
9329    #[tokio::test(flavor = "multi_thread")]
9330    async fn test_lines_perpendicular() {
9331        let initial_source = "\
9332sketch(on = XY) {
9333  line(start = [var 1, var 2], end = [var 3, var 4])
9334  line(start = [var 5, var 6], end = [var 7, var 8])
9335}
9336";
9337
9338        let program = Program::parse(initial_source).unwrap().0.unwrap();
9339
9340        let mut frontend = FrontendState::new();
9341
9342        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9343        let mock_ctx = ExecutorContext::new_mock(None).await;
9344        let version = Version(0);
9345
9346        frontend.hack_set_program(&ctx, program).await.unwrap();
9347        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9348        let sketch_id = sketch_object.id;
9349        let sketch = expect_sketch(sketch_object);
9350        let line1_id = *sketch.segments.get(2).unwrap();
9351        let line2_id = *sketch.segments.get(5).unwrap();
9352
9353        let constraint = Constraint::Perpendicular(Perpendicular {
9354            lines: vec![line1_id, line2_id],
9355        });
9356        let (src_delta, scene_delta) = frontend
9357            .add_constraint(&mock_ctx, version, sketch_id, constraint)
9358            .await
9359            .unwrap();
9360        assert_eq!(
9361            src_delta.text.as_str(),
9362            "\
9363sketch(on = XY) {
9364  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9365  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9366  perpendicular([line1, line2])
9367}
9368"
9369        );
9370        assert_eq!(
9371            scene_delta.new_graph.objects.len(),
9372            9,
9373            "{:#?}",
9374            scene_delta.new_graph.objects
9375        );
9376
9377        ctx.close().await;
9378        mock_ctx.close().await;
9379    }
9380
9381    #[tokio::test(flavor = "multi_thread")]
9382    async fn test_lines_angle() {
9383        let initial_source = "\
9384sketch(on = XY) {
9385  line(start = [var 1, var 2], end = [var 3, var 4])
9386  line(start = [var 5, var 6], end = [var 7, var 8])
9387}
9388";
9389
9390        let program = Program::parse(initial_source).unwrap().0.unwrap();
9391
9392        let mut frontend = FrontendState::new();
9393
9394        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9395        let mock_ctx = ExecutorContext::new_mock(None).await;
9396        let version = Version(0);
9397
9398        frontend.hack_set_program(&ctx, program).await.unwrap();
9399        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9400        let sketch_id = sketch_object.id;
9401        let sketch = expect_sketch(sketch_object);
9402        let line1_id = *sketch.segments.get(2).unwrap();
9403        let line2_id = *sketch.segments.get(5).unwrap();
9404
9405        let constraint = Constraint::Angle(Angle {
9406            lines: vec![line1_id, line2_id],
9407            angle: Number {
9408                value: 30.0,
9409                units: NumericSuffix::Deg,
9410            },
9411            source: Default::default(),
9412        });
9413        let (src_delta, scene_delta) = frontend
9414            .add_constraint(&mock_ctx, version, sketch_id, constraint)
9415            .await
9416            .unwrap();
9417        assert_eq!(
9418            src_delta.text.as_str(),
9419            // The lack indentation is a formatter bug.
9420            "\
9421sketch(on = XY) {
9422  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9423  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9424  angle([line1, line2]) == 30deg
9425}
9426"
9427        );
9428        assert_eq!(
9429            scene_delta.new_graph.objects.len(),
9430            9,
9431            "{:#?}",
9432            scene_delta.new_graph.objects
9433        );
9434
9435        ctx.close().await;
9436        mock_ctx.close().await;
9437    }
9438
9439    #[tokio::test(flavor = "multi_thread")]
9440    async fn test_segments_tangent() {
9441        let initial_source = "\
9442sketch(on = XY) {
9443  line(start = [var 1, var 2], end = [var 3, var 4])
9444  arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
9445}
9446";
9447
9448        let program = Program::parse(initial_source).unwrap().0.unwrap();
9449
9450        let mut frontend = FrontendState::new();
9451
9452        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9453        let mock_ctx = ExecutorContext::new_mock(None).await;
9454        let version = Version(0);
9455
9456        frontend.hack_set_program(&ctx, program).await.unwrap();
9457        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9458        let sketch_id = sketch_object.id;
9459        let sketch = expect_sketch(sketch_object);
9460        let line1_id = *sketch.segments.get(2).unwrap();
9461        let arc1_id = *sketch.segments.get(6).unwrap();
9462
9463        let constraint = Constraint::Tangent(Tangent {
9464            input: vec![line1_id, arc1_id],
9465        });
9466        let (src_delta, scene_delta) = frontend
9467            .add_constraint(&mock_ctx, version, sketch_id, constraint)
9468            .await
9469            .unwrap();
9470        assert_eq!(
9471            src_delta.text.as_str(),
9472            "\
9473sketch(on = XY) {
9474  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9475  arc1 = arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
9476  tangent([line1, arc1])
9477}
9478"
9479        );
9480        assert_eq!(
9481            scene_delta.new_graph.objects.len(),
9482            10,
9483            "{:#?}",
9484            scene_delta.new_graph.objects
9485        );
9486
9487        ctx.close().await;
9488        mock_ctx.close().await;
9489    }
9490
9491    #[tokio::test(flavor = "multi_thread")]
9492    async fn test_point_midpoint() {
9493        let initial_source = "\
9494sketch(on = XY) {
9495  point(at = [var 1, var 1])
9496  line(start = [var 0, var 0], end = [var 6, var 4])
9497}
9498";
9499
9500        let program = Program::parse(initial_source).unwrap().0.unwrap();
9501
9502        let mut frontend = FrontendState::new();
9503
9504        let ctx = ExecutorContext::new_mock(None).await;
9505        let version = Version(0);
9506
9507        frontend.program = program.clone();
9508        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9509        frontend.update_state_after_exec(outcome, true);
9510        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9511        let sketch_id = sketch_object.id;
9512        let sketch = expect_sketch(sketch_object);
9513        let point_id = *sketch.segments.first().unwrap();
9514        let line_id = *sketch.segments.get(3).unwrap();
9515
9516        let constraint = Constraint::Midpoint(Midpoint {
9517            point: point_id,
9518            segment: line_id,
9519        });
9520        let (src_delta, scene_delta) = frontend
9521            .add_constraint(&ctx, version, sketch_id, constraint)
9522            .await
9523            .unwrap();
9524        assert_eq!(
9525            src_delta.text.as_str(),
9526            "\
9527sketch(on = XY) {
9528  point1 = point(at = [var 1, var 1])
9529  line1 = line(start = [var 0, var 0], end = [var 6, var 4])
9530  midpoint(line1, point = point1)
9531}
9532"
9533        );
9534        assert_eq!(
9535            scene_delta.new_graph.objects.len(),
9536            7,
9537            "{:#?}",
9538            scene_delta.new_graph.objects
9539        );
9540
9541        ctx.close().await;
9542    }
9543
9544    #[tokio::test(flavor = "multi_thread")]
9545    async fn test_point_arc_midpoint() {
9546        let initial_source = "\
9547sketch(on = XY) {
9548  point(at = [var 6, var 3])
9549  arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
9550}
9551";
9552
9553        let program = Program::parse(initial_source).unwrap().0.unwrap();
9554
9555        let mut frontend = FrontendState::new();
9556
9557        let ctx = ExecutorContext::new_mock(None).await;
9558        let version = Version(0);
9559
9560        frontend.program = program.clone();
9561        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9562        frontend.update_state_after_exec(outcome, true);
9563        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9564        let sketch_id = sketch_object.id;
9565        let sketch = expect_sketch(sketch_object);
9566        let point_id = *sketch.segments.first().unwrap();
9567        let arc_id = *sketch.segments.get(4).unwrap();
9568
9569        let constraint = Constraint::Midpoint(Midpoint {
9570            point: point_id,
9571            segment: arc_id,
9572        });
9573        let (src_delta, scene_delta) = frontend
9574            .add_constraint(&ctx, version, sketch_id, constraint)
9575            .await
9576            .unwrap();
9577        assert_eq!(
9578            src_delta.text.as_str(),
9579            "\
9580sketch(on = XY) {
9581  point1 = point(at = [var 6, var 3])
9582  arc1 = arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
9583  midpoint(arc1, point = point1)
9584}
9585"
9586        );
9587        assert_eq!(
9588            scene_delta.new_graph.objects.len(),
9589            8,
9590            "{:#?}",
9591            scene_delta.new_graph.objects
9592        );
9593
9594        ctx.close().await;
9595    }
9596
9597    #[tokio::test(flavor = "multi_thread")]
9598    async fn test_sketch_on_face_simple() {
9599        let initial_source = "\
9600len = 2mm
9601cube = startSketchOn(XY)
9602  |> startProfile(at = [0, 0])
9603  |> line(end = [len, 0], tag = $side)
9604  |> line(end = [0, len])
9605  |> line(end = [-len, 0])
9606  |> line(end = [0, -len])
9607  |> close()
9608  |> extrude(length = len)
9609
9610face = faceOf(cube, face = side)
9611";
9612
9613        let program = Program::parse(initial_source).unwrap().0.unwrap();
9614
9615        let mut frontend = FrontendState::new();
9616
9617        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9618        let mock_ctx = ExecutorContext::new_mock(None).await;
9619        let version = Version(0);
9620
9621        frontend.hack_set_program(&ctx, program).await.unwrap();
9622        let face_object = find_first_face_object(&frontend.scene_graph).unwrap();
9623        let face_id = face_object.id;
9624
9625        let sketch_args = SketchCtor {
9626            on: Plane::Object(face_id),
9627        };
9628        let (_src_delta, scene_delta, sketch_id) = frontend
9629            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
9630            .await
9631            .unwrap();
9632        assert_eq!(sketch_id, ObjectId(2));
9633        assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
9634        let sketch_object = &scene_delta.new_graph.objects[2];
9635        assert_eq!(sketch_object.id, ObjectId(2));
9636        assert_eq!(
9637            sketch_object.kind,
9638            ObjectKind::Sketch(Sketch {
9639                args: SketchCtor {
9640                    on: Plane::Object(face_id),
9641                },
9642                plane: face_id,
9643                segments: vec![],
9644                constraints: vec![],
9645            })
9646        );
9647        assert_eq!(scene_delta.new_graph.objects.len(), 8);
9648
9649        ctx.close().await;
9650        mock_ctx.close().await;
9651    }
9652
9653    #[tokio::test(flavor = "multi_thread")]
9654    async fn test_sketch_on_wall_artifact_from_region_extrude() {
9655        let initial_source = "\
9656s = sketch(on = YZ) {
9657  line1 = line(start = [0, 0], end = [0, 1])
9658  line2 = line(start = [0, 1], end = [1, 1])
9659  line3 = line(start = [1, 1], end = [0, 0])
9660}
9661region001 = region(point = [0.1, 0.1], sketch = s)
9662extrude001 = extrude(region001, length = 5)
9663";
9664
9665        let program = Program::parse(initial_source).unwrap().0.unwrap();
9666
9667        let mut frontend = FrontendState::new();
9668        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9669        let version = Version(0);
9670
9671        frontend.hack_set_program(&ctx, program).await.unwrap();
9672        let wall_object_id = find_first_wall_object_id(&frontend.scene_graph).expect("expected a wall object");
9673
9674        let sketch_args = SketchCtor {
9675            on: Plane::Object(wall_object_id),
9676        };
9677        let (src_delta, _scene_delta, _sketch_id) = frontend
9678            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
9679            .await
9680            .unwrap();
9681        assert!(src_delta.text.contains("faceOf(extrude001, face = region001.tags."));
9682
9683        ctx.close().await;
9684    }
9685
9686    #[tokio::test(flavor = "multi_thread")]
9687    async fn test_sketch_on_wall_artifact_from_split_region_extrude() {
9688        let initial_source = "\
9689sketch001 = sketch(on = YZ) {
9690  line1 = line(start = [var 0.49, var -0.39], end = [var 6.52, var -0.39])
9691  line2 = line(start = [var 6.52, var -0.39], end = [var 6.52, var 4.9])
9692  line3 = line(start = [var 6.52, var 4.9], end = [var 0.49, var 4.9])
9693  line4 = line(start = [var 0.49, var 4.9], end = [var 0.49, var -0.39])
9694  coincident([line1.end, line2.start])
9695  coincident([line2.end, line3.start])
9696  coincident([line3.end, line4.start])
9697  coincident([line4.end, line1.start])
9698  parallel([line2, line4])
9699  parallel([line3, line1])
9700  perpendicular([line1, line2])
9701  horizontal(line3)
9702  line5 = line(start = [2.35, 6.65], end = [5.89, -2.7])
9703}
9704region001 = region(point = [3.1, 3.74], sketch = sketch001)
9705extrude001 = extrude(region001, length = 5)
9706";
9707
9708        let program = Program::parse(initial_source).unwrap().0.unwrap();
9709
9710        let mut frontend = FrontendState::new();
9711        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9712        let version = Version(0);
9713
9714        frontend.hack_set_program(&ctx, program).await.unwrap();
9715        let wall_object_id = find_first_wall_object_id(&frontend.scene_graph).expect("expected a wall object");
9716
9717        let sketch_args = SketchCtor {
9718            on: Plane::Object(wall_object_id),
9719        };
9720        let (src_delta, _scene_delta, _sketch_id) = frontend
9721            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
9722            .await
9723            .unwrap();
9724        assert!(src_delta.text.contains("faceOf(extrude001, face = region001.tags."));
9725
9726        ctx.close().await;
9727    }
9728
9729    #[tokio::test(flavor = "multi_thread")]
9730    async fn test_sketch_on_plane_incremental() {
9731        let initial_source = "\
9732len = 2mm
9733cube = startSketchOn(XY)
9734  |> startProfile(at = [0, 0])
9735  |> line(end = [len, 0], tag = $side)
9736  |> line(end = [0, len])
9737  |> line(end = [-len, 0])
9738  |> line(end = [0, -len])
9739  |> close()
9740  |> extrude(length = len)
9741
9742plane = planeOf(cube, face = side)
9743";
9744
9745        let program = Program::parse(initial_source).unwrap().0.unwrap();
9746
9747        let mut frontend = FrontendState::new();
9748
9749        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9750        let mock_ctx = ExecutorContext::new_mock(None).await;
9751        let version = Version(0);
9752
9753        frontend.hack_set_program(&ctx, program).await.unwrap();
9754        // Find the last plane since the first plane is the XY plane.
9755        let plane_object = frontend
9756            .scene_graph
9757            .objects
9758            .iter()
9759            .rev()
9760            .find(|object| matches!(&object.kind, ObjectKind::Plane(_)))
9761            .unwrap();
9762        let plane_id = plane_object.id;
9763
9764        let sketch_args = SketchCtor {
9765            on: Plane::Object(plane_id),
9766        };
9767        let (src_delta, scene_delta, sketch_id) = frontend
9768            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
9769            .await
9770            .unwrap();
9771        assert_eq!(
9772            src_delta.text.as_str(),
9773            "\
9774len = 2mm
9775cube = startSketchOn(XY)
9776  |> startProfile(at = [0, 0])
9777  |> line(end = [len, 0], tag = $side)
9778  |> line(end = [0, len])
9779  |> line(end = [-len, 0])
9780  |> line(end = [0, -len])
9781  |> close()
9782  |> extrude(length = len)
9783
9784plane = planeOf(cube, face = side)
9785sketch001 = sketch(on = plane) {
9786}
9787"
9788        );
9789        assert_eq!(sketch_id, ObjectId(2));
9790        assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
9791        let sketch_object = &scene_delta.new_graph.objects[2];
9792        assert_eq!(sketch_object.id, ObjectId(2));
9793        assert_eq!(
9794            sketch_object.kind,
9795            ObjectKind::Sketch(Sketch {
9796                args: SketchCtor {
9797                    on: Plane::Object(plane_id),
9798                },
9799                plane: plane_id,
9800                segments: vec![],
9801                constraints: vec![],
9802            })
9803        );
9804        assert_eq!(scene_delta.new_graph.objects.len(), 9);
9805
9806        let plane_object = scene_delta.new_graph.objects.get(plane_id.0).unwrap();
9807        assert_eq!(plane_object.id, plane_id);
9808        assert_eq!(plane_object.kind, ObjectKind::Plane(Plane::Object(plane_id)));
9809
9810        ctx.close().await;
9811        mock_ctx.close().await;
9812    }
9813
9814    #[tokio::test(flavor = "multi_thread")]
9815    async fn test_new_sketch_uses_unique_variable_name() {
9816        let initial_source = "\
9817sketch1 = sketch(on = XY) {
9818}
9819";
9820
9821        let program = Program::parse(initial_source).unwrap().0.unwrap();
9822
9823        let mut frontend = FrontendState::new();
9824        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9825        let version = Version(0);
9826
9827        frontend.hack_set_program(&ctx, program).await.unwrap();
9828
9829        let sketch_args = SketchCtor {
9830            on: Plane::Default(PlaneName::Yz),
9831        };
9832        let (src_delta, _, _) = frontend
9833            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
9834            .await
9835            .unwrap();
9836
9837        assert_eq!(
9838            src_delta.text.as_str(),
9839            "\
9840sketch1 = sketch(on = XY) {
9841}
9842sketch001 = sketch(on = YZ) {
9843}
9844"
9845        );
9846
9847        ctx.close().await;
9848    }
9849
9850    #[tokio::test(flavor = "multi_thread")]
9851    async fn test_new_sketch_twice_using_same_plane() {
9852        let initial_source = "\
9853sketch1 = sketch(on = XY) {
9854}
9855";
9856
9857        let program = Program::parse(initial_source).unwrap().0.unwrap();
9858
9859        let mut frontend = FrontendState::new();
9860        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9861        let version = Version(0);
9862
9863        frontend.hack_set_program(&ctx, program).await.unwrap();
9864
9865        let sketch_args = SketchCtor {
9866            on: Plane::Default(PlaneName::Xy),
9867        };
9868        let (src_delta, _, _) = frontend
9869            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
9870            .await
9871            .unwrap();
9872
9873        assert_eq!(
9874            src_delta.text.as_str(),
9875            "\
9876sketch1 = sketch(on = XY) {
9877}
9878sketch001 = sketch(on = XY) {
9879}
9880"
9881        );
9882
9883        ctx.close().await;
9884    }
9885
9886    #[tokio::test(flavor = "multi_thread")]
9887    async fn test_sketch_mode_reuses_cached_on_expression() {
9888        let initial_source = "\
9889width = 2mm
9890sketch(on = offsetPlane(XY, offset = width)) {
9891  line1 = line(start = [var 0, var 0], end = [var 1mm, var 0])
9892  distance([line1.start, line1.end]) == width
9893}
9894";
9895        let program = Program::parse(initial_source).unwrap().0.unwrap();
9896
9897        let mut frontend = FrontendState::new();
9898        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9899        let mock_ctx = ExecutorContext::new_mock(None).await;
9900        let version = Version(0);
9901        let project_id = ProjectId(0);
9902        let file_id = FileId(0);
9903
9904        frontend.hack_set_program(&ctx, program).await.unwrap();
9905        let initial_object_count = frontend.scene_graph.objects.len();
9906        let sketch_id = find_first_sketch_object(&frontend.scene_graph)
9907            .expect("Expected sketch object to exist")
9908            .id;
9909
9910        // Entering sketch mode should reuse cached `on` expression state
9911        // (offsetPlane result), not fail or create extra on-surface objects.
9912        let scene_delta = frontend
9913            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
9914            .await
9915            .unwrap();
9916        assert_eq!(scene_delta.new_graph.objects.len(), initial_object_count);
9917
9918        // A follow-up sketch-mode execution should keep the same stable object
9919        // graph shape as well.
9920        let (_src_delta, scene_delta) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
9921        assert_eq!(scene_delta.new_graph.objects.len(), initial_object_count);
9922
9923        ctx.close().await;
9924        mock_ctx.close().await;
9925    }
9926
9927    #[tokio::test(flavor = "multi_thread")]
9928    async fn test_multiple_sketch_blocks() {
9929        let initial_source = "\
9930// Cube that requires the engine.
9931width = 2
9932sketch001 = startSketchOn(XY)
9933profile001 = startProfile(sketch001, at = [0, 0])
9934  |> yLine(length = width, tag = $seg1)
9935  |> xLine(length = width)
9936  |> yLine(length = -width)
9937  |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
9938  |> close()
9939extrude001 = extrude(profile001, length = width)
9940
9941// Get a value that requires the engine.
9942x = segLen(seg1)
9943
9944// Triangle with side length 2*x.
9945sketch(on = XY) {
9946  line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
9947  line2 = line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
9948  coincident([line1.end, line2.start])
9949  line3 = line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
9950  coincident([line2.end, line3.start])
9951  coincident([line3.end, line1.start])
9952  equalLength([line3, line1])
9953  equalLength([line1, line2])
9954  distance([line1.start, line1.end]) == 2*x
9955}
9956
9957// Line segment with length x.
9958sketch2 = sketch(on = XY) {
9959  line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
9960  distance([line1.start, line1.end]) == x
9961}
9962";
9963
9964        let program = Program::parse(initial_source).unwrap().0.unwrap();
9965
9966        let mut frontend = FrontendState::new();
9967
9968        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9969        let mock_ctx = ExecutorContext::new_mock(None).await;
9970        let version = Version(0);
9971        let project_id = ProjectId(0);
9972        let file_id = FileId(0);
9973
9974        frontend.hack_set_program(&ctx, program).await.unwrap();
9975        let sketch_objects = frontend
9976            .scene_graph
9977            .objects
9978            .iter()
9979            .filter(|obj| matches!(obj.kind, ObjectKind::Sketch(_)))
9980            .collect::<Vec<_>>();
9981        let sketch1_id = sketch_objects.first().unwrap().id;
9982        let sketch2_id = sketch_objects.get(1).unwrap().id;
9983        // First point in sketch1.
9984        let point1_id = ObjectId(sketch1_id.0 + 1);
9985        // First point in sketch2.
9986        let point2_id = ObjectId(sketch2_id.0 + 1);
9987
9988        // Edit the first sketch. Objects before the sketch block should be
9989        // present from execution cache so that we can sketch on prior planes,
9990        // for example. Objects after the first sketch block should not be
9991        // present since those statements are skipped in sketch mode.
9992        //
9993        // - startSketchOn(XY) Plane 1
9994        // - sketch on=XY Plane 1
9995        // - Sketch block 16
9996        let scene_delta = frontend
9997            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch1_id)
9998            .await
9999            .unwrap();
10000        assert_eq!(
10001            scene_delta.new_graph.objects.len(),
10002            18,
10003            "{:#?}",
10004            scene_delta.new_graph.objects
10005        );
10006
10007        // Edit a point in the first sketch.
10008        let point_ctor = PointCtor {
10009            position: Point2d {
10010                x: Expr::Var(Number {
10011                    value: 1.0,
10012                    units: NumericSuffix::Mm,
10013                }),
10014                y: Expr::Var(Number {
10015                    value: 2.0,
10016                    units: NumericSuffix::Mm,
10017                }),
10018            },
10019        };
10020        let segments = vec![ExistingSegmentCtor {
10021            id: point1_id,
10022            ctor: SegmentCtor::Point(point_ctor),
10023        }];
10024        let (src_delta, _) = frontend
10025            .edit_segments(&mock_ctx, version, sketch1_id, segments)
10026            .await
10027            .unwrap();
10028        // Only the first sketch block changes.
10029        assert_eq!(
10030            src_delta.text.as_str(),
10031            "\
10032// Cube that requires the engine.
10033width = 2
10034sketch001 = startSketchOn(XY)
10035profile001 = startProfile(sketch001, at = [0, 0])
10036  |> yLine(length = width, tag = $seg1)
10037  |> xLine(length = width)
10038  |> yLine(length = -width)
10039  |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
10040  |> close()
10041extrude001 = extrude(profile001, length = width)
10042
10043// Get a value that requires the engine.
10044x = segLen(seg1)
10045
10046// Triangle with side length 2*x.
10047sketch(on = XY) {
10048  line1 = line(start = [var 1mm, var 2mm], end = [var 2.32mm, var -1.78mm])
10049  line2 = line(start = [var 2.32mm, var -1.78mm], end = [var -1.61mm, var -1.03mm])
10050  coincident([line1.end, line2.start])
10051  line3 = line(start = [var -1.61mm, var -1.03mm], end = [var 1mm, var 2mm])
10052  coincident([line2.end, line3.start])
10053  coincident([line3.end, line1.start])
10054  equalLength([line3, line1])
10055  equalLength([line1, line2])
10056  distance([line1.start, line1.end]) == 2 * x
10057}
10058
10059// Line segment with length x.
10060sketch2 = sketch(on = XY) {
10061  line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
10062  distance([line1.start, line1.end]) == x
10063}
10064"
10065        );
10066
10067        // Execute mock to simulate drag end.
10068        let (src_delta, _) = frontend.execute_mock(&mock_ctx, version, sketch1_id).await.unwrap();
10069        // Only the first sketch block changes.
10070        assert_eq!(
10071            src_delta.text.as_str(),
10072            "\
10073// Cube that requires the engine.
10074width = 2
10075sketch001 = startSketchOn(XY)
10076profile001 = startProfile(sketch001, at = [0, 0])
10077  |> yLine(length = width, tag = $seg1)
10078  |> xLine(length = width)
10079  |> yLine(length = -width)
10080  |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
10081  |> close()
10082extrude001 = extrude(profile001, length = width)
10083
10084// Get a value that requires the engine.
10085x = segLen(seg1)
10086
10087// Triangle with side length 2*x.
10088sketch(on = XY) {
10089  line1 = line(start = [var 1mm, var 2mm], end = [var 1.28mm, var -0.78mm])
10090  line2 = line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
10091  coincident([line1.end, line2.start])
10092  line3 = line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
10093  coincident([line2.end, line3.start])
10094  coincident([line3.end, line1.start])
10095  equalLength([line3, line1])
10096  equalLength([line1, line2])
10097  distance([line1.start, line1.end]) == 2 * x
10098}
10099
10100// Line segment with length x.
10101sketch2 = sketch(on = XY) {
10102  line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
10103  distance([line1.start, line1.end]) == x
10104}
10105"
10106        );
10107        // Exit sketch. Objects from the entire program should be present.
10108        //
10109        // - startSketchOn(XY) Plane 1
10110        // - sketch on=XY Plane 1
10111        // - Sketch block 16
10112        // - sketch on=XY cached
10113        // - Sketch block 5
10114        let scene = frontend.exit_sketch(&ctx, version, sketch1_id).await.unwrap();
10115        assert_eq!(scene.objects.len(), 30, "{:#?}", scene.objects);
10116
10117        // Edit the second sketch.
10118        //
10119        // - startSketchOn(XY) Plane 1
10120        // - sketch on=XY Plane 1
10121        // - Sketch block 16
10122        // - sketch on=XY cached
10123        // - Sketch block 5
10124        let scene_delta = frontend
10125            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch2_id)
10126            .await
10127            .unwrap();
10128        assert_eq!(
10129            scene_delta.new_graph.objects.len(),
10130            24,
10131            "{:#?}",
10132            scene_delta.new_graph.objects
10133        );
10134
10135        // Edit a point in the second sketch.
10136        let point_ctor = PointCtor {
10137            position: Point2d {
10138                x: Expr::Var(Number {
10139                    value: 3.0,
10140                    units: NumericSuffix::Mm,
10141                }),
10142                y: Expr::Var(Number {
10143                    value: 4.0,
10144                    units: NumericSuffix::Mm,
10145                }),
10146            },
10147        };
10148        let segments = vec![ExistingSegmentCtor {
10149            id: point2_id,
10150            ctor: SegmentCtor::Point(point_ctor),
10151        }];
10152        let (src_delta, _) = frontend
10153            .edit_segments(&mock_ctx, version, sketch2_id, segments)
10154            .await
10155            .unwrap();
10156        // Only the second sketch block changes.
10157        assert_eq!(
10158            src_delta.text.as_str(),
10159            "\
10160// Cube that requires the engine.
10161width = 2
10162sketch001 = startSketchOn(XY)
10163profile001 = startProfile(sketch001, at = [0, 0])
10164  |> yLine(length = width, tag = $seg1)
10165  |> xLine(length = width)
10166  |> yLine(length = -width)
10167  |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
10168  |> close()
10169extrude001 = extrude(profile001, length = width)
10170
10171// Get a value that requires the engine.
10172x = segLen(seg1)
10173
10174// Triangle with side length 2*x.
10175sketch(on = XY) {
10176  line1 = line(start = [var 1mm, var 2mm], end = [var 1.28mm, var -0.78mm])
10177  line2 = line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
10178  coincident([line1.end, line2.start])
10179  line3 = line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
10180  coincident([line2.end, line3.start])
10181  coincident([line3.end, line1.start])
10182  equalLength([line3, line1])
10183  equalLength([line1, line2])
10184  distance([line1.start, line1.end]) == 2 * x
10185}
10186
10187// Line segment with length x.
10188sketch2 = sketch(on = XY) {
10189  line1 = line(start = [var 3mm, var 4mm], end = [var 2.32mm, var 2.12mm])
10190  distance([line1.start, line1.end]) == x
10191}
10192"
10193        );
10194
10195        // Execute mock to simulate drag end.
10196        let (src_delta, _) = frontend.execute_mock(&mock_ctx, version, sketch2_id).await.unwrap();
10197        // Only the second sketch block changes.
10198        assert_eq!(
10199            src_delta.text.as_str(),
10200            "\
10201// Cube that requires the engine.
10202width = 2
10203sketch001 = startSketchOn(XY)
10204profile001 = startProfile(sketch001, at = [0, 0])
10205  |> yLine(length = width, tag = $seg1)
10206  |> xLine(length = width)
10207  |> yLine(length = -width)
10208  |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
10209  |> close()
10210extrude001 = extrude(profile001, length = width)
10211
10212// Get a value that requires the engine.
10213x = segLen(seg1)
10214
10215// Triangle with side length 2*x.
10216sketch(on = XY) {
10217  line1 = line(start = [var 1mm, var 2mm], end = [var 1.28mm, var -0.78mm])
10218  line2 = line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
10219  coincident([line1.end, line2.start])
10220  line3 = line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
10221  coincident([line2.end, line3.start])
10222  coincident([line3.end, line1.start])
10223  equalLength([line3, line1])
10224  equalLength([line1, line2])
10225  distance([line1.start, line1.end]) == 2 * x
10226}
10227
10228// Line segment with length x.
10229sketch2 = sketch(on = XY) {
10230  line1 = line(start = [var 3mm, var 4mm], end = [var 1.28mm, var -0.78mm])
10231  distance([line1.start, line1.end]) == x
10232}
10233"
10234        );
10235
10236        ctx.close().await;
10237        mock_ctx.close().await;
10238    }
10239
10240    #[tokio::test(flavor = "multi_thread")]
10241    async fn test_exit_sketch_without_changes_allows_entering_next_sketch() {
10242        clear_mem_cache().await;
10243
10244        let source = r#"sketch001 = sketch(on = XZ) {
10245  circle1 = circle(start = [var -1.96mm, var 2.77mm], center = [var -2.69mm, var 3.44mm])
10246}
10247sketch002 = sketch(on = XY) {
10248  line1 = line(start = [var 0mm, var 0mm], end = [var 4.68mm, var 0mm])
10249  line2 = line(start = [var 4.68mm, var 0mm], end = [var 4.68mm, var 2.96mm])
10250  line3 = line(start = [var 4.68mm, var 2.96mm], end = [var 0mm, var 2.96mm])
10251  line4 = line(start = [var 0mm, var 2.96mm], end = [var 0mm, var 0mm])
10252  coincident([line1.end, line2.start])
10253  coincident([line2.end, line3.start])
10254  coincident([line3.end, line4.start])
10255  coincident([line4.end, line1.start])
10256  parallel([line2, line4])
10257  parallel([line3, line1])
10258  perpendicular([line1, line2])
10259  horizontal(line3)
10260  coincident([line1.start, ORIGIN])
10261}
10262"#;
10263
10264        let program = Program::parse(source).unwrap().0.unwrap();
10265        let mut frontend = FrontendState::new();
10266        let ctx = ExecutorContext::new_with_engine(
10267            std::sync::Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().unwrap())),
10268            Default::default(),
10269        );
10270        let mock_ctx = ExecutorContext::new_mock(None).await;
10271        let version = Version(0);
10272        let project_id = ProjectId(0);
10273        let file_id = FileId(0);
10274
10275        frontend.hack_set_program(&ctx, program).await.unwrap();
10276        let sketch_objects = frontend
10277            .scene_graph
10278            .objects
10279            .iter()
10280            .filter(|object| matches!(object.kind, ObjectKind::Sketch(_)))
10281            .collect::<Vec<_>>();
10282        assert_eq!(sketch_objects.len(), 2, "{:#?}", frontend.scene_graph.objects);
10283
10284        let sketch1_id = sketch_objects[0].id;
10285        let sketch2_id = sketch_objects[1].id;
10286
10287        frontend
10288            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch1_id)
10289            .await
10290            .unwrap();
10291        frontend.exit_sketch(&ctx, version, sketch1_id).await.unwrap();
10292
10293        let scene_delta = frontend
10294            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch2_id)
10295            .await
10296            .unwrap();
10297        assert_eq!(scene_delta.new_graph.sketch_mode, Some(sketch2_id));
10298
10299        clear_mem_cache().await;
10300        ctx.close().await;
10301        mock_ctx.close().await;
10302    }
10303
10304    // Regression tests: operations on source code with extra whitespace/newlines.
10305    // These test that NodePath-based lookups work correctly when source ranges
10306    // are shifted by extra whitespace that wouldn't be present after formatting.
10307
10308    #[tokio::test(flavor = "multi_thread")]
10309    async fn test_extra_newlines_after_settings_edit_sketch_add_point() {
10310        // Extra newlines after @settings line - this shifts all source ranges.
10311        let initial_source = "@settings(defaultLengthUnit = mm)
10312
10313
10314
10315sketch001 = sketch(on = XY) {
10316  point(at = [1in, 2in])
10317}
10318";
10319
10320        let program = Program::parse(initial_source).unwrap().0.unwrap();
10321        let mut frontend = FrontendState::new();
10322
10323        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10324        let mock_ctx = ExecutorContext::new_mock(None).await;
10325        let version = Version(0);
10326        let project_id = ProjectId(0);
10327        let file_id = FileId(0);
10328
10329        frontend.hack_set_program(&ctx, program).await.unwrap();
10330        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10331        let sketch_id = sketch_object.id;
10332
10333        // Edit sketch should succeed despite extra newlines.
10334        frontend
10335            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
10336            .await
10337            .unwrap();
10338
10339        // Add a new point to the sketch.
10340        let point_ctor = PointCtor {
10341            position: Point2d {
10342                x: Expr::Number(Number {
10343                    value: 5.0,
10344                    units: NumericSuffix::Mm,
10345                }),
10346                y: Expr::Number(Number {
10347                    value: 6.0,
10348                    units: NumericSuffix::Mm,
10349                }),
10350            },
10351        };
10352        let segment = SegmentCtor::Point(point_ctor);
10353        let (src_delta, scene_delta) = frontend
10354            .add_segment(&mock_ctx, version, sketch_id, segment, None)
10355            .await
10356            .unwrap();
10357        // After adding a point, the source should be reformatted with standard whitespace.
10358        assert!(
10359            src_delta.text.contains("point(at = [5mm, 6mm])"),
10360            "Expected new point in source, got: {}",
10361            src_delta.text
10362        );
10363        assert!(!scene_delta.new_objects.is_empty());
10364
10365        ctx.close().await;
10366        mock_ctx.close().await;
10367    }
10368
10369    #[tokio::test(flavor = "multi_thread")]
10370    async fn test_extra_newlines_after_settings_add_line_to_empty_sketch() {
10371        // Extra newlines after @settings, with an empty sketch block.
10372        let initial_source = "@settings(defaultLengthUnit = mm)
10373
10374
10375
10376s = sketch(on = XY) {}
10377";
10378
10379        let program = Program::parse(initial_source).unwrap().0.unwrap();
10380        let mut frontend = FrontendState::new();
10381
10382        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10383        let mock_ctx = ExecutorContext::new_mock(None).await;
10384        let version = Version(0);
10385
10386        frontend.hack_set_program(&ctx, program).await.unwrap();
10387        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10388        let sketch_id = sketch_object.id;
10389
10390        let line_ctor = LineCtor {
10391            start: Point2d {
10392                x: Expr::Number(Number {
10393                    value: 0.0,
10394                    units: NumericSuffix::Mm,
10395                }),
10396                y: Expr::Number(Number {
10397                    value: 0.0,
10398                    units: NumericSuffix::Mm,
10399                }),
10400            },
10401            end: Point2d {
10402                x: Expr::Number(Number {
10403                    value: 10.0,
10404                    units: NumericSuffix::Mm,
10405                }),
10406                y: Expr::Number(Number {
10407                    value: 10.0,
10408                    units: NumericSuffix::Mm,
10409                }),
10410            },
10411            construction: None,
10412        };
10413        let segment = SegmentCtor::Line(line_ctor);
10414        let (src_delta, scene_delta) = frontend
10415            .add_segment(&mock_ctx, version, sketch_id, segment, None)
10416            .await
10417            .unwrap();
10418        assert!(
10419            src_delta.text.contains("line(start = [0mm, 0mm], end = [10mm, 10mm])"),
10420            "Expected line in source, got: {}",
10421            src_delta.text
10422        );
10423        // Line creates start point, end point, and line segment.
10424        assert_eq!(scene_delta.new_objects.len(), 3);
10425
10426        ctx.close().await;
10427        mock_ctx.close().await;
10428    }
10429
10430    #[tokio::test(flavor = "multi_thread")]
10431    async fn test_extra_newlines_between_operations_edit_line() {
10432        // Extra newlines between @settings and sketch, and inside the sketch block.
10433        let initial_source = "@settings(defaultLengthUnit = mm)
10434
10435
10436sketch001 = sketch(on = XY) {
10437
10438  line1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm])
10439
10440}
10441";
10442
10443        let program = Program::parse(initial_source).unwrap().0.unwrap();
10444        let mut frontend = FrontendState::new();
10445
10446        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10447        let mock_ctx = ExecutorContext::new_mock(None).await;
10448        let version = Version(0);
10449        let project_id = ProjectId(0);
10450        let file_id = FileId(0);
10451
10452        frontend.hack_set_program(&ctx, program).await.unwrap();
10453        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10454        let sketch_id = sketch_object.id;
10455        let sketch = expect_sketch(sketch_object);
10456
10457        // Extract segment IDs before edit_sketch borrows frontend mutably.
10458        let line_id = sketch
10459            .segments
10460            .iter()
10461            .copied()
10462            .find(|seg_id| {
10463                matches!(
10464                    &frontend.scene_graph.objects[seg_id.0].kind,
10465                    ObjectKind::Segment {
10466                        segment: Segment::Line(_)
10467                    }
10468                )
10469            })
10470            .expect("Expected a line segment in sketch");
10471
10472        // Enter sketch edit mode.
10473        frontend
10474            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
10475            .await
10476            .unwrap();
10477
10478        // Edit the line.
10479        let line_ctor = LineCtor {
10480            start: Point2d {
10481                x: Expr::Var(Number {
10482                    value: 1.0,
10483                    units: NumericSuffix::Mm,
10484                }),
10485                y: Expr::Var(Number {
10486                    value: 2.0,
10487                    units: NumericSuffix::Mm,
10488                }),
10489            },
10490            end: Point2d {
10491                x: Expr::Var(Number {
10492                    value: 13.0,
10493                    units: NumericSuffix::Mm,
10494                }),
10495                y: Expr::Var(Number {
10496                    value: 14.0,
10497                    units: NumericSuffix::Mm,
10498                }),
10499            },
10500            construction: None,
10501        };
10502        let segments = vec![ExistingSegmentCtor {
10503            id: line_id,
10504            ctor: SegmentCtor::Line(line_ctor),
10505        }];
10506        let (src_delta, _scene_delta) = frontend
10507            .edit_segments(&mock_ctx, version, sketch_id, segments)
10508            .await
10509            .unwrap();
10510        assert!(
10511            src_delta
10512                .text
10513                .contains("line(start = [var 1mm, var 2mm], end = [var 13mm, var 14mm])"),
10514            "Expected edited line in source, got: {}",
10515            src_delta.text
10516        );
10517
10518        ctx.close().await;
10519        mock_ctx.close().await;
10520    }
10521
10522    #[tokio::test(flavor = "multi_thread")]
10523    async fn test_extra_newlines_delete_segment() {
10524        // Extra whitespace before and after the sketch block.
10525        let initial_source = "@settings(defaultLengthUnit = mm)
10526
10527
10528
10529sketch001 = sketch(on = XY) {
10530  circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
10531}
10532";
10533
10534        let program = Program::parse(initial_source).unwrap().0.unwrap();
10535        let mut frontend = FrontendState::new();
10536
10537        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10538        let mock_ctx = ExecutorContext::new_mock(None).await;
10539        let version = Version(0);
10540
10541        frontend.hack_set_program(&ctx, program).await.unwrap();
10542        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10543        let sketch_id = sketch_object.id;
10544        let sketch = expect_sketch(sketch_object);
10545
10546        // The sketch should have 3 segments: start point, center point, and the circle.
10547        assert_eq!(sketch.segments.len(), 3);
10548        let circle_id = sketch.segments[2];
10549
10550        // Delete the circle despite extra newlines in original source.
10551        let (src_delta, scene_delta) = frontend
10552            .delete_objects(&mock_ctx, version, sketch_id, vec![], vec![circle_id])
10553            .await
10554            .unwrap();
10555        assert!(
10556            src_delta.text.contains("sketch(on = XY) {"),
10557            "Expected sketch block in source, got: {}",
10558            src_delta.text
10559        );
10560        let new_sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10561        let new_sketch = expect_sketch(new_sketch_object);
10562        assert_eq!(new_sketch.segments.len(), 0);
10563
10564        ctx.close().await;
10565        mock_ctx.close().await;
10566    }
10567
10568    #[tokio::test(flavor = "multi_thread")]
10569    async fn test_unformatted_source_add_arc() {
10570        // Source with inconsistent whitespace - tabs, extra spaces, multiple blank lines.
10571        let initial_source = "@settings(defaultLengthUnit = mm)
10572
10573
10574
10575
10576sketch001 = sketch(on = XY) {
10577}
10578";
10579
10580        let program = Program::parse(initial_source).unwrap().0.unwrap();
10581        let mut frontend = FrontendState::new();
10582
10583        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10584        let mock_ctx = ExecutorContext::new_mock(None).await;
10585        let version = Version(0);
10586
10587        frontend.hack_set_program(&ctx, program).await.unwrap();
10588        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10589        let sketch_id = sketch_object.id;
10590
10591        let arc_ctor = ArcCtor {
10592            start: Point2d {
10593                x: Expr::Var(Number {
10594                    value: 5.0,
10595                    units: NumericSuffix::Mm,
10596                }),
10597                y: Expr::Var(Number {
10598                    value: 0.0,
10599                    units: NumericSuffix::Mm,
10600                }),
10601            },
10602            end: Point2d {
10603                x: Expr::Var(Number {
10604                    value: 0.0,
10605                    units: NumericSuffix::Mm,
10606                }),
10607                y: Expr::Var(Number {
10608                    value: 5.0,
10609                    units: NumericSuffix::Mm,
10610                }),
10611            },
10612            center: Point2d {
10613                x: Expr::Var(Number {
10614                    value: 0.0,
10615                    units: NumericSuffix::Mm,
10616                }),
10617                y: Expr::Var(Number {
10618                    value: 0.0,
10619                    units: NumericSuffix::Mm,
10620                }),
10621            },
10622            construction: None,
10623        };
10624        let segment = SegmentCtor::Arc(arc_ctor);
10625        let (src_delta, scene_delta) = frontend
10626            .add_segment(&mock_ctx, version, sketch_id, segment, None)
10627            .await
10628            .unwrap();
10629        assert!(
10630            src_delta
10631                .text
10632                .contains("arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])"),
10633            "Expected arc in source, got: {}",
10634            src_delta.text
10635        );
10636        assert!(!scene_delta.new_objects.is_empty());
10637
10638        ctx.close().await;
10639        mock_ctx.close().await;
10640    }
10641
10642    #[tokio::test(flavor = "multi_thread")]
10643    async fn test_extra_newlines_add_circle() {
10644        // Extra blank lines between settings and sketch.
10645        let initial_source = "@settings(defaultLengthUnit = mm)
10646
10647
10648
10649sketch001 = sketch(on = XY) {
10650}
10651";
10652
10653        let program = Program::parse(initial_source).unwrap().0.unwrap();
10654        let mut frontend = FrontendState::new();
10655
10656        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10657        let mock_ctx = ExecutorContext::new_mock(None).await;
10658        let version = Version(0);
10659
10660        frontend.hack_set_program(&ctx, program).await.unwrap();
10661        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10662        let sketch_id = sketch_object.id;
10663
10664        let circle_ctor = CircleCtor {
10665            start: Point2d {
10666                x: Expr::Var(Number {
10667                    value: 5.0,
10668                    units: NumericSuffix::Mm,
10669                }),
10670                y: Expr::Var(Number {
10671                    value: 0.0,
10672                    units: NumericSuffix::Mm,
10673                }),
10674            },
10675            center: Point2d {
10676                x: Expr::Var(Number {
10677                    value: 0.0,
10678                    units: NumericSuffix::Mm,
10679                }),
10680                y: Expr::Var(Number {
10681                    value: 0.0,
10682                    units: NumericSuffix::Mm,
10683                }),
10684            },
10685            construction: None,
10686        };
10687        let segment = SegmentCtor::Circle(circle_ctor);
10688        let (src_delta, scene_delta) = frontend
10689            .add_segment(&mock_ctx, version, sketch_id, segment, None)
10690            .await
10691            .unwrap();
10692        assert!(
10693            src_delta
10694                .text
10695                .contains("circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])"),
10696            "Expected circle in source, got: {}",
10697            src_delta.text
10698        );
10699        assert!(!scene_delta.new_objects.is_empty());
10700
10701        ctx.close().await;
10702        mock_ctx.close().await;
10703    }
10704
10705    #[tokio::test(flavor = "multi_thread")]
10706    async fn test_extra_newlines_add_constraint() {
10707        // Extra newlines with a sketch containing two lines - add a coincident constraint.
10708        let initial_source = "@settings(defaultLengthUnit = mm)
10709
10710
10711
10712sketch001 = sketch(on = XY) {
10713  line1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm])
10714  line2 = line(start = [var 10mm, var 10mm], end = [var 20mm, var 0mm])
10715}
10716";
10717
10718        let program = Program::parse(initial_source).unwrap().0.unwrap();
10719        let mut frontend = FrontendState::new();
10720
10721        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10722        let mock_ctx = ExecutorContext::new_mock(None).await;
10723        let version = Version(0);
10724        let project_id = ProjectId(0);
10725        let file_id = FileId(0);
10726
10727        frontend.hack_set_program(&ctx, program).await.unwrap();
10728        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10729        let sketch_id = sketch_object.id;
10730        let sketch = expect_sketch(sketch_object);
10731
10732        // Extract segment data before edit_sketch borrows frontend mutably.
10733        let line_ids: Vec<ObjectId> = sketch
10734            .segments
10735            .iter()
10736            .copied()
10737            .filter(|seg_id| {
10738                matches!(
10739                    &frontend.scene_graph.objects[seg_id.0].kind,
10740                    ObjectKind::Segment {
10741                        segment: Segment::Line(_)
10742                    }
10743                )
10744            })
10745            .collect();
10746        assert_eq!(line_ids.len(), 2, "Expected two line segments");
10747
10748        let line1 = &frontend.scene_graph.objects[line_ids[0].0];
10749        let ObjectKind::Segment {
10750            segment: Segment::Line(line1_data),
10751        } = &line1.kind
10752        else {
10753            panic!("Expected line");
10754        };
10755        let line2 = &frontend.scene_graph.objects[line_ids[1].0];
10756        let ObjectKind::Segment {
10757            segment: Segment::Line(line2_data),
10758        } = &line2.kind
10759        else {
10760            panic!("Expected line");
10761        };
10762
10763        // Build constraint before entering sketch mode.
10764        let constraint = Constraint::Coincident(Coincident {
10765            segments: vec![line1_data.end.into(), line2_data.start.into()],
10766        });
10767
10768        // Enter sketch edit mode.
10769        frontend
10770            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
10771            .await
10772            .unwrap();
10773        let (src_delta, _scene_delta) = frontend
10774            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10775            .await
10776            .unwrap();
10777        assert!(
10778            src_delta.text.contains("coincident("),
10779            "Expected coincident constraint in source, got: {}",
10780            src_delta.text
10781        );
10782
10783        ctx.close().await;
10784        mock_ctx.close().await;
10785    }
10786
10787    #[tokio::test(flavor = "multi_thread")]
10788    async fn test_extra_newlines_add_line_then_edit_line() {
10789        // Extra newlines after @settings - add a line, then edit it.
10790        let initial_source = "@settings(defaultLengthUnit = mm)
10791
10792
10793
10794sketch001 = sketch(on = XY) {
10795}
10796";
10797
10798        let program = Program::parse(initial_source).unwrap().0.unwrap();
10799        let mut frontend = FrontendState::new();
10800
10801        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10802        let mock_ctx = ExecutorContext::new_mock(None).await;
10803        let version = Version(0);
10804
10805        frontend.hack_set_program(&ctx, program).await.unwrap();
10806        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10807        let sketch_id = sketch_object.id;
10808
10809        // Add a line.
10810        let line_ctor = LineCtor {
10811            start: Point2d {
10812                x: Expr::Number(Number {
10813                    value: 0.0,
10814                    units: NumericSuffix::Mm,
10815                }),
10816                y: Expr::Number(Number {
10817                    value: 0.0,
10818                    units: NumericSuffix::Mm,
10819                }),
10820            },
10821            end: Point2d {
10822                x: Expr::Number(Number {
10823                    value: 10.0,
10824                    units: NumericSuffix::Mm,
10825                }),
10826                y: Expr::Number(Number {
10827                    value: 10.0,
10828                    units: NumericSuffix::Mm,
10829                }),
10830            },
10831            construction: None,
10832        };
10833        let segment = SegmentCtor::Line(line_ctor);
10834        let (src_delta, scene_delta) = frontend
10835            .add_segment(&mock_ctx, version, sketch_id, segment, None)
10836            .await
10837            .unwrap();
10838        assert!(
10839            src_delta.text.contains("line(start = [0mm, 0mm], end = [10mm, 10mm])"),
10840            "Expected line in source after add, got: {}",
10841            src_delta.text
10842        );
10843        // Line creates start point, end point, and line segment.
10844        let line_id = *scene_delta.new_objects.last().unwrap();
10845
10846        // Edit the line.
10847        let line_ctor = LineCtor {
10848            start: Point2d {
10849                x: Expr::Number(Number {
10850                    value: 1.0,
10851                    units: NumericSuffix::Mm,
10852                }),
10853                y: Expr::Number(Number {
10854                    value: 2.0,
10855                    units: NumericSuffix::Mm,
10856                }),
10857            },
10858            end: Point2d {
10859                x: Expr::Number(Number {
10860                    value: 13.0,
10861                    units: NumericSuffix::Mm,
10862                }),
10863                y: Expr::Number(Number {
10864                    value: 14.0,
10865                    units: NumericSuffix::Mm,
10866                }),
10867            },
10868            construction: None,
10869        };
10870        let segments = vec![ExistingSegmentCtor {
10871            id: line_id,
10872            ctor: SegmentCtor::Line(line_ctor),
10873        }];
10874        let (src_delta, scene_delta) = frontend
10875            .edit_segments(&mock_ctx, version, sketch_id, segments)
10876            .await
10877            .unwrap();
10878        assert!(
10879            src_delta.text.contains("line(start = [1mm, 2mm], end = [13mm, 14mm])"),
10880            "Expected edited line in source, got: {}",
10881            src_delta.text
10882        );
10883        assert_eq!(scene_delta.new_objects, vec![]);
10884
10885        ctx.close().await;
10886        mock_ctx.close().await;
10887    }
10888}