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