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