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