Skip to main content

kcl_lib/
frontend.rs

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