Skip to main content

kcl_lib/
frontend.rs

1use std::cell::Cell;
2use std::collections::HashMap;
3use std::collections::HashSet;
4use std::collections::VecDeque;
5use std::ops::ControlFlow;
6
7use indexmap::IndexMap;
8use kcl_error::CompilationIssue;
9use kcl_error::SourceRange;
10use kittycad_modeling_cmds::units::UnitLength;
11use serde::Serialize;
12
13use crate::ExecOutcome;
14use crate::ExecutorContext;
15use crate::KclError;
16use crate::KclErrorWithOutputs;
17use crate::Program;
18use crate::collections::AhashIndexSet;
19#[cfg(feature = "artifact-graph")]
20use crate::execution::Artifact;
21#[cfg(feature = "artifact-graph")]
22use crate::execution::ArtifactGraph;
23#[cfg(feature = "artifact-graph")]
24use crate::execution::CapSubType;
25use crate::execution::MockConfig;
26use crate::execution::SKETCH_BLOCK_PARAM_ON;
27use crate::execution::cache::SketchModeState;
28use crate::execution::cache::clear_mem_cache;
29use crate::execution::cache::read_old_memory;
30use crate::execution::cache::write_old_memory;
31use crate::fmt::format_number_literal;
32use crate::front::Angle;
33use crate::front::ArcCtor;
34use crate::front::CircleCtor;
35use crate::front::Distance;
36use crate::front::EqualRadius;
37use crate::front::Error;
38use crate::front::ExecResult;
39use crate::front::FixedPoint;
40use crate::front::Freedom;
41use crate::front::LinesEqualLength;
42use crate::front::Midpoint;
43use crate::front::Object;
44use crate::front::Parallel;
45use crate::front::Perpendicular;
46use crate::front::PointCtor;
47use crate::front::Symmetric;
48use crate::front::Tangent;
49use crate::frontend::api::Expr;
50use crate::frontend::api::FileId;
51use crate::frontend::api::Number;
52use crate::frontend::api::ObjectId;
53use crate::frontend::api::ObjectKind;
54use crate::frontend::api::Plane;
55use crate::frontend::api::ProjectId;
56use crate::frontend::api::RestoreSketchCheckpointOutcome;
57use crate::frontend::api::SceneGraph;
58use crate::frontend::api::SceneGraphDelta;
59use crate::frontend::api::SketchCheckpointId;
60use crate::frontend::api::SourceDelta;
61use crate::frontend::api::SourceRef;
62use crate::frontend::api::Version;
63use crate::frontend::modify::find_defined_names;
64use crate::frontend::modify::next_free_name;
65use crate::frontend::modify::next_free_name_with_padding;
66use crate::frontend::sketch::Coincident;
67use crate::frontend::sketch::Constraint;
68use crate::frontend::sketch::ConstraintSegment;
69use crate::frontend::sketch::Diameter;
70use crate::frontend::sketch::ExistingSegmentCtor;
71use crate::frontend::sketch::Horizontal;
72use crate::frontend::sketch::LineCtor;
73use crate::frontend::sketch::Point2d;
74use crate::frontend::sketch::Radius;
75use crate::frontend::sketch::Segment;
76use crate::frontend::sketch::SegmentCtor;
77use crate::frontend::sketch::SketchApi;
78use crate::frontend::sketch::SketchCtor;
79use crate::frontend::sketch::Vertical;
80use crate::frontend::traverse::MutateBodyItem;
81use crate::frontend::traverse::TraversalReturn;
82use crate::frontend::traverse::Visitor;
83use crate::frontend::traverse::dfs_mut;
84use crate::id::IncIdGenerator;
85use crate::parsing::ast::types as ast;
86use crate::pretty::NumericSuffix;
87use crate::std::constraints::LinesAtAngleKind;
88use crate::walk::NodeMut;
89use crate::walk::Visitable;
90
91pub(crate) mod api;
92pub(crate) mod modify;
93pub(crate) mod sketch;
94
95pub const MAX_SKETCH_CHECKPOINTS: usize = 100;
96
97#[derive(Debug, Clone)]
98struct SketchCheckpoint {
99    id: SketchCheckpointId,
100    source: SourceDelta,
101    program: Program,
102    scene_graph: SceneGraph,
103    exec_outcome: ExecOutcome,
104    point_freedom_cache: HashMap<ObjectId, Freedom>,
105    mock_memory: Option<SketchModeState>,
106}
107mod traverse;
108pub(crate) mod trim;
109
110struct ArcSizeConstraintParams {
111    points: Vec<ObjectId>,
112    function_name: &'static str,
113    value: f64,
114    units: NumericSuffix,
115    constraint_type_name: &'static str,
116}
117
118const POINT_FN: &str = "point";
119const POINT_AT_PARAM: &str = "at";
120const LINE_FN: &str = "line";
121const LINE_VARIABLE: &str = "line";
122const LINE_START_PARAM: &str = "start";
123const LINE_END_PARAM: &str = "end";
124const ARC_FN: &str = "arc";
125const ARC_VARIABLE: &str = "arc";
126const ARC_START_PARAM: &str = "start";
127const ARC_END_PARAM: &str = "end";
128const ARC_CENTER_PARAM: &str = "center";
129const CIRCLE_FN: &str = "circle";
130const CIRCLE_VARIABLE: &str = "circle";
131const CIRCLE_START_PARAM: &str = "start";
132const CIRCLE_CENTER_PARAM: &str = "center";
133const LABEL_POSITION_PARAM: &str = "labelPosition";
134
135const COINCIDENT_FN: &str = "coincident";
136const DIAMETER_FN: &str = "diameter";
137const DISTANCE_FN: &str = "distance";
138const FIXED_FN: &str = "fixed";
139const ANGLE_FN: &str = "angle";
140const HORIZONTAL_DISTANCE_FN: &str = "horizontalDistance";
141const VERTICAL_DISTANCE_FN: &str = "verticalDistance";
142const EQUAL_LENGTH_FN: &str = "equalLength";
143const EQUAL_RADIUS_FN: &str = "equalRadius";
144const HORIZONTAL_FN: &str = "horizontal";
145const MIDPOINT_FN: &str = "midpoint";
146const MIDPOINT_POINT_PARAM: &str = "point";
147const RADIUS_FN: &str = "radius";
148const SYMMETRIC_FN: &str = "symmetric";
149const SYMMETRIC_AXIS_PARAM: &str = "axis";
150const TANGENT_FN: &str = "tangent";
151const VERTICAL_FN: &str = "vertical";
152
153const LINE_PROPERTY_START: &str = "start";
154const LINE_PROPERTY_END: &str = "end";
155
156const ARC_PROPERTY_START: &str = "start";
157const ARC_PROPERTY_END: &str = "end";
158const ARC_PROPERTY_CENTER: &str = "center";
159const CIRCLE_PROPERTY_START: &str = "start";
160const CIRCLE_PROPERTY_CENTER: &str = "center";
161
162const CONSTRUCTION_PARAM: &str = "construction";
163
164#[derive(Debug, Clone, Copy)]
165enum EditDeleteKind {
166    Edit,
167    DeleteNonSketch,
168}
169
170impl EditDeleteKind {
171    /// Returns true if this edit is any type of deletion.
172    fn is_delete(&self) -> bool {
173        match self {
174            EditDeleteKind::Edit => false,
175            EditDeleteKind::DeleteNonSketch => true,
176        }
177    }
178
179    fn to_change_kind(self) -> ChangeKind {
180        match self {
181            EditDeleteKind::Edit => ChangeKind::Edit,
182            EditDeleteKind::DeleteNonSketch => ChangeKind::Delete,
183        }
184    }
185}
186
187#[derive(Debug, Clone, Copy)]
188enum ChangeKind {
189    Add,
190    Edit,
191    Delete,
192    None,
193}
194
195#[derive(Debug, Clone, Serialize, ts_rs::TS)]
196#[ts(export, export_to = "FrontendApi.ts")]
197#[serde(tag = "type")]
198pub enum SetProgramOutcome {
199    #[serde(rename_all = "camelCase")]
200    Success {
201        scene_graph: Box<SceneGraph>,
202        exec_outcome: Box<ExecOutcome>,
203        checkpoint_id: Option<SketchCheckpointId>,
204    },
205    #[serde(rename_all = "camelCase")]
206    ExecFailure { error: Box<KclErrorWithOutputs> },
207}
208
209#[derive(Debug, Clone)]
210pub struct FrontendState {
211    program: Program,
212    scene_graph: SceneGraph,
213    /// Stores the last known freedom value for each point object.
214    /// This allows us to preserve freedom values when freedom analysis isn't run.
215    point_freedom_cache: HashMap<ObjectId, Freedom>,
216    sketch_checkpoints: VecDeque<SketchCheckpoint>,
217    sketch_checkpoint_id_gen: IncIdGenerator<u64>,
218}
219
220impl Default for FrontendState {
221    fn default() -> Self {
222        Self::new()
223    }
224}
225
226impl FrontendState {
227    pub fn new() -> Self {
228        Self {
229            program: Program::empty(),
230            scene_graph: SceneGraph {
231                project: ProjectId(0),
232                file: FileId(0),
233                version: Version(0),
234                objects: Default::default(),
235                settings: Default::default(),
236                sketch_mode: Default::default(),
237            },
238            point_freedom_cache: HashMap::new(),
239            sketch_checkpoints: VecDeque::new(),
240            sketch_checkpoint_id_gen: IncIdGenerator::new(1),
241        }
242    }
243
244    /// Get a reference to the scene graph
245    pub fn scene_graph(&self) -> &SceneGraph {
246        &self.scene_graph
247    }
248
249    pub fn default_length_unit(&self) -> UnitLength {
250        self.program
251            .meta_settings()
252            .ok()
253            .flatten()
254            .map(|settings| settings.default_length_units)
255            .unwrap_or(UnitLength::Millimeters)
256    }
257
258    pub async fn create_sketch_checkpoint(&mut self, exec_outcome: ExecOutcome) -> api::Result<SketchCheckpointId> {
259        let checkpoint_id = SketchCheckpointId::new(self.sketch_checkpoint_id_gen.next_id());
260
261        let checkpoint = SketchCheckpoint {
262            id: checkpoint_id,
263            source: SourceDelta {
264                text: source_from_ast(&self.program.ast),
265            },
266            program: self.program.clone(),
267            scene_graph: self.scene_graph.clone(),
268            exec_outcome,
269            point_freedom_cache: self.point_freedom_cache.clone(),
270            mock_memory: read_old_memory().await,
271        };
272
273        self.sketch_checkpoints.push_back(checkpoint);
274        while self.sketch_checkpoints.len() > MAX_SKETCH_CHECKPOINTS {
275            self.sketch_checkpoints.pop_front();
276        }
277
278        Ok(checkpoint_id)
279    }
280
281    pub async fn restore_sketch_checkpoint(
282        &mut self,
283        checkpoint_id: SketchCheckpointId,
284    ) -> api::Result<RestoreSketchCheckpointOutcome> {
285        let checkpoint = self
286            .sketch_checkpoints
287            .iter()
288            .find(|checkpoint| checkpoint.id == checkpoint_id)
289            .cloned()
290            .ok_or_else(|| Error {
291                msg: format!("Sketch checkpoint not found: {checkpoint_id:?}"),
292            })?;
293
294        self.program = checkpoint.program;
295        self.scene_graph = checkpoint.scene_graph.clone();
296        self.point_freedom_cache = checkpoint.point_freedom_cache;
297
298        if let Some(mock_memory) = checkpoint.mock_memory {
299            write_old_memory(mock_memory).await;
300        } else {
301            clear_mem_cache().await;
302        }
303
304        Ok(RestoreSketchCheckpointOutcome {
305            source_delta: checkpoint.source,
306            scene_graph_delta: SceneGraphDelta {
307                new_graph: checkpoint.scene_graph,
308                new_objects: Vec::new(),
309                invalidates_ids: true,
310                exec_outcome: checkpoint.exec_outcome,
311            },
312        })
313    }
314
315    pub fn clear_sketch_checkpoints(&mut self) {
316        self.sketch_checkpoints.clear();
317    }
318}
319
320impl SketchApi for FrontendState {
321    async fn execute_mock(
322        &mut self,
323        ctx: &ExecutorContext,
324        _version: Version,
325        sketch: ObjectId,
326    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
327        let sketch_block_ref =
328            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
329
330        let mut truncated_program = self.program.clone();
331        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::None)
332            .map_err(KclErrorWithOutputs::no_outputs)?;
333
334        // Execute.
335        let outcome = ctx
336            .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch))
337            .await?;
338        let new_source = source_from_ast(&self.program.ast);
339        let src_delta = SourceDelta { text: new_source };
340        // MockConfig::default() has freedom_analysis: true
341        let outcome = self.update_state_after_exec(outcome, true);
342        let scene_graph_delta = SceneGraphDelta {
343            new_graph: self.scene_graph.clone(),
344            new_objects: Default::default(),
345            invalidates_ids: false,
346            exec_outcome: outcome,
347        };
348        Ok((src_delta, scene_graph_delta))
349    }
350
351    async fn new_sketch(
352        &mut self,
353        ctx: &ExecutorContext,
354        _project: ProjectId,
355        _file: FileId,
356        _version: Version,
357        args: SketchCtor,
358    ) -> ExecResult<(SourceDelta, SceneGraphDelta, ObjectId)> {
359        // TODO: Check version.
360
361        let mut new_ast = self.program.ast.clone();
362        // Create updated KCL source from args.
363        let mut plane_ast =
364            sketch_on_ast_expr(&mut new_ast, &self.scene_graph, &args.on).map_err(KclErrorWithOutputs::no_outputs)?;
365        let mut defined_names = find_defined_names(&new_ast);
366        let is_face_of_expr = matches!(
367            &plane_ast,
368            ast::Expr::CallExpressionKw(call) if call.callee.name.name == "faceOf"
369        );
370        if is_face_of_expr {
371            let face_name = next_free_name_with_padding("face", &defined_names)
372                .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
373            let face_decl = ast::VariableDeclaration::new(
374                ast::VariableDeclarator::new(&face_name, plane_ast),
375                ast::ItemVisibility::Default,
376                ast::VariableKind::Const,
377            );
378            new_ast
379                .body
380                .push(ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(
381                    face_decl,
382                ))));
383            defined_names.insert(face_name.clone());
384            plane_ast = ast::Expr::Name(Box::new(ast::Name::new(&face_name)));
385        }
386        let sketch_ast = ast::SketchBlock {
387            arguments: vec![ast::LabeledArg {
388                label: Some(ast::Identifier::new(SKETCH_BLOCK_PARAM_ON)),
389                arg: plane_ast,
390            }],
391            body: Default::default(),
392            is_being_edited: false,
393            non_code_meta: Default::default(),
394            digest: None,
395        };
396        // Add a sketch block as a variable declaration directly, avoiding
397        // source-range mutation on a no-src node.
398        let sketch_name = next_free_name_with_padding("sketch", &defined_names)
399            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
400        let sketch_decl = ast::VariableDeclaration::new(
401            ast::VariableDeclarator::new(
402                &sketch_name,
403                ast::Expr::SketchBlock(Box::new(ast::Node::no_src(sketch_ast))),
404            ),
405            ast::ItemVisibility::Default,
406            ast::VariableKind::Const,
407        );
408        new_ast
409            .body
410            .push(ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(
411                sketch_decl,
412            ))));
413        // Convert to string source to create real source ranges.
414        let new_source = source_from_ast(&new_ast);
415        // Parse the new source.
416        let (new_program, errors) = Program::parse(&new_source)
417            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
418        if !errors.is_empty() {
419            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
420                "Error parsing KCL source after adding sketch: {errors:?}"
421            ))));
422        }
423        let Some(new_program) = new_program else {
424            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
425                "No AST produced after adding sketch".to_owned(),
426            )));
427        };
428
429        // Make sure to only set this if there are no errors.
430        self.program = new_program.clone();
431
432        // We need to do an engine execute so that the plane object gets created
433        // and is cached.
434        let outcome = ctx.run_with_caching(new_program.clone()).await?;
435        let freedom_analysis_ran = true;
436
437        let outcome = self.update_state_after_exec(outcome, freedom_analysis_ran);
438
439        let Some(sketch_id) = self
440            .scene_graph
441            .objects
442            .iter()
443            .filter_map(|object| match object.kind {
444                ObjectKind::Sketch(_) => Some(object.id),
445                _ => None,
446            })
447            .max_by_key(|id| id.0)
448        else {
449            return Err(KclErrorWithOutputs::from_error_outcome(
450                KclError::refactor("No objects in scene graph after adding sketch".to_owned()),
451                outcome,
452            ));
453        };
454        // Store the object in the scene.
455        self.scene_graph.sketch_mode = Some(sketch_id);
456
457        let src_delta = SourceDelta { text: new_source };
458        let scene_graph_delta = SceneGraphDelta {
459            new_graph: self.scene_graph.clone(),
460            invalidates_ids: false,
461            new_objects: vec![sketch_id],
462            exec_outcome: outcome,
463        };
464        Ok((src_delta, scene_graph_delta, sketch_id))
465    }
466
467    async fn edit_sketch(
468        &mut self,
469        ctx: &ExecutorContext,
470        _project: ProjectId,
471        _file: FileId,
472        _version: Version,
473        sketch: ObjectId,
474    ) -> ExecResult<SceneGraphDelta> {
475        // TODO: Check version.
476
477        // Look up existing sketch.
478        let sketch_object = self.scene_graph.objects.get(sketch.0).ok_or_else(|| {
479            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
480        })?;
481        let ObjectKind::Sketch(_) = &sketch_object.kind else {
482            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
483                "Object is not a sketch, it is {}",
484                sketch_object.kind.human_friendly_kind_with_article()
485            ))));
486        };
487        let sketch_block_ref = expect_single_node_ref(sketch_object).map_err(KclErrorWithOutputs::no_outputs)?;
488
489        // Enter sketch mode by setting the sketch_mode.
490        self.scene_graph.sketch_mode = Some(sketch);
491
492        // Truncate after the sketch block for mock execution.
493        let mut truncated_program = self.program.clone();
494        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::None)
495            .map_err(KclErrorWithOutputs::no_outputs)?;
496
497        // Execute in mock mode to ensure state is up to date. The caller will
498        // want freedom analysis to display segments correctly.
499        let outcome = ctx
500            .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch))
501            .await?;
502
503        // MockConfig::default() has freedom_analysis: true
504        let outcome = self.update_state_after_exec(outcome, true);
505        let scene_graph_delta = SceneGraphDelta {
506            new_graph: self.scene_graph.clone(),
507            invalidates_ids: false,
508            new_objects: Vec::new(),
509            exec_outcome: outcome,
510        };
511        Ok(scene_graph_delta)
512    }
513
514    async fn exit_sketch(
515        &mut self,
516        ctx: &ExecutorContext,
517        _version: Version,
518        sketch: ObjectId,
519    ) -> ExecResult<SceneGraph> {
520        // TODO: Check version.
521        #[cfg(not(target_arch = "wasm32"))]
522        let _ = sketch;
523        #[cfg(target_arch = "wasm32")]
524        if self.scene_graph.sketch_mode != Some(sketch) {
525            web_sys::console::warn_1(
526                &format!(
527                    "WARNING: exit_sketch: current state's sketch mode ID doesn't match the given sketch ID; state={:#?}, given={sketch:?}",
528                    &self.scene_graph.sketch_mode
529                )
530                .into(),
531            );
532        }
533        self.scene_graph.sketch_mode = None;
534
535        // Execute.
536        let outcome = ctx.run_with_caching(self.program.clone()).await?;
537
538        // exit_sketch doesn't run freedom analysis, just clears sketch_mode
539        self.update_state_after_exec(outcome, false);
540
541        Ok(self.scene_graph.clone())
542    }
543
544    async fn delete_sketch(
545        &mut self,
546        ctx: &ExecutorContext,
547        _version: Version,
548        sketch: ObjectId,
549    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
550        // TODO: Check version.
551
552        let mut new_ast = self.program.ast.clone();
553
554        // Look up existing sketch.
555        let sketch_id = sketch;
556        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
557            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
558        })?;
559        let ObjectKind::Sketch(_) = &sketch_object.kind else {
560            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
561                "Object is not a sketch, it is {}",
562                sketch_object.kind.human_friendly_kind_with_article(),
563            ))));
564        };
565
566        // Modify the AST to remove the sketch.
567        self.mutate_ast(&mut new_ast, sketch_id, AstMutateCommand::DeleteNode)
568            .map_err(KclErrorWithOutputs::no_outputs)?;
569
570        self.execute_after_delete_sketch(ctx, &mut new_ast).await
571    }
572
573    async fn add_segment(
574        &mut self,
575        ctx: &ExecutorContext,
576        _version: Version,
577        sketch: ObjectId,
578        segment: SegmentCtor,
579        _label: Option<String>,
580    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
581        // TODO: Check version.
582        match segment {
583            SegmentCtor::Point(ctor) => self.add_point(ctx, sketch, ctor).await,
584            SegmentCtor::Line(ctor) => self.add_line(ctx, sketch, ctor).await,
585            SegmentCtor::Arc(ctor) => self.add_arc(ctx, sketch, ctor).await,
586            SegmentCtor::Circle(ctor) => self.add_circle(ctx, sketch, ctor).await,
587        }
588    }
589
590    async fn edit_segments(
591        &mut self,
592        ctx: &ExecutorContext,
593        _version: Version,
594        sketch: ObjectId,
595        segments: Vec<ExistingSegmentCtor>,
596    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
597        // TODO: Check version.
598        let sketch_block_ref =
599            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
600
601        let mut new_ast = self.program.ast.clone();
602        let mut segment_ids_edited = AhashIndexSet::with_capacity_and_hasher(segments.len(), Default::default());
603
604        // segment_ids_edited still has to be the original segments (not final_edits), otherwise the owner segments
605        // are passed to `execute_after_edit` which changes the result of the solver, causing tests to fail.
606        for segment in &segments {
607            segment_ids_edited.insert(segment.id);
608        }
609
610        // Preprocess segments into a final_edits vector to handle if segments contains:
611        // - edit start point of line1 (as SegmentCtor::Point)
612        // - edit end point of line1 (as SegmentCtor::Point)
613        //
614        // This would result in only the end point to be updated because edit_point() clones line1's ctor from
615        // scene_graph, but this is still the old ctor because self.scene_graph is only updated after the loop finishes.
616        //
617        // To fix this, and other cases when the same point is edited from multiple elements in the segments Vec
618        // we apply all edits in order to final_edits in a way that owned point edits result in line edits,
619        // so the above example would result in a single line1 edit:
620        // - the first start point edit creates a new line edit entry in final_edits
621        // - the second end point edit finds this line edit and mutates the end position only.
622        //
623        // The result is that segments are flattened into a single IndexMap of edits by their owners, later edits overriding earlier ones.
624        let mut final_edits: IndexMap<ObjectId, SegmentCtor> = IndexMap::new();
625
626        for segment in segments {
627            let segment_id = segment.id;
628            match segment.ctor {
629                SegmentCtor::Point(ctor) => {
630                    // Find the owner, if any (point -> line / arc)
631                    if let Some(segment_object) = self.scene_graph.objects.get(segment_id.0)
632                        && let ObjectKind::Segment { segment } = &segment_object.kind
633                        && let Segment::Point(point) = segment
634                        && let Some(owner_id) = point.owner
635                        && let Some(owner_object) = self.scene_graph.objects.get(owner_id.0)
636                        && let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind
637                    {
638                        match owner_segment {
639                            Segment::Line(line) if line.start == segment_id || line.end == segment_id => {
640                                if let Some(existing) = final_edits.get_mut(&owner_id) {
641                                    let SegmentCtor::Line(line_ctor) = existing else {
642                                        return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
643                                            "Internal: Expected line ctor for owner, but found {}",
644                                            existing.human_friendly_kind_with_article()
645                                        ))));
646                                    };
647                                    // Line owner is already in final_edits -> apply this point edit
648                                    if line.start == segment_id {
649                                        line_ctor.start = ctor.position;
650                                    } else {
651                                        line_ctor.end = ctor.position;
652                                    }
653                                } else if let SegmentCtor::Line(line_ctor) = &line.ctor {
654                                    // Line owner is not in final_edits yet -> create it
655                                    let mut line_ctor = line_ctor.clone();
656                                    if line.start == segment_id {
657                                        line_ctor.start = ctor.position;
658                                    } else {
659                                        line_ctor.end = ctor.position;
660                                    }
661                                    final_edits.insert(owner_id, SegmentCtor::Line(line_ctor));
662                                } else {
663                                    // This should never run..
664                                    return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
665                                        "Internal: Line does not have line ctor, but found {}",
666                                        line.ctor.human_friendly_kind_with_article()
667                                    ))));
668                                }
669                                continue;
670                            }
671                            Segment::Arc(arc)
672                                if arc.start == segment_id || arc.end == segment_id || arc.center == segment_id =>
673                            {
674                                if let Some(existing) = final_edits.get_mut(&owner_id) {
675                                    let SegmentCtor::Arc(arc_ctor) = existing else {
676                                        return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
677                                            "Internal: Expected arc ctor for owner, but found {}",
678                                            existing.human_friendly_kind_with_article()
679                                        ))));
680                                    };
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                                } else if let SegmentCtor::Arc(arc_ctor) = &arc.ctor {
689                                    let mut arc_ctor = arc_ctor.clone();
690                                    if arc.start == segment_id {
691                                        arc_ctor.start = ctor.position;
692                                    } else if arc.end == segment_id {
693                                        arc_ctor.end = ctor.position;
694                                    } else {
695                                        arc_ctor.center = ctor.position;
696                                    }
697                                    final_edits.insert(owner_id, SegmentCtor::Arc(arc_ctor));
698                                } else {
699                                    return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
700                                        "Internal: Arc does not have arc ctor, but found {}",
701                                        arc.ctor.human_friendly_kind_with_article()
702                                    ))));
703                                }
704                                continue;
705                            }
706                            Segment::Circle(circle) if circle.start == segment_id || circle.center == segment_id => {
707                                if let Some(existing) = final_edits.get_mut(&owner_id) {
708                                    let SegmentCtor::Circle(circle_ctor) = existing else {
709                                        return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
710                                            "Internal: Expected circle ctor for owner, but found {}",
711                                            existing.human_friendly_kind_with_article()
712                                        ))));
713                                    };
714                                    if circle.start == segment_id {
715                                        circle_ctor.start = ctor.position;
716                                    } else {
717                                        circle_ctor.center = ctor.position;
718                                    }
719                                } else if let SegmentCtor::Circle(circle_ctor) = &circle.ctor {
720                                    let mut circle_ctor = circle_ctor.clone();
721                                    if circle.start == segment_id {
722                                        circle_ctor.start = ctor.position;
723                                    } else {
724                                        circle_ctor.center = ctor.position;
725                                    }
726                                    final_edits.insert(owner_id, SegmentCtor::Circle(circle_ctor));
727                                } else {
728                                    return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
729                                        "Internal: Circle does not have circle ctor, but found {}",
730                                        circle.ctor.human_friendly_kind_with_article()
731                                    ))));
732                                }
733                                continue;
734                            }
735                            _ => {}
736                        }
737                    }
738
739                    // No owner, it's an individual point
740                    final_edits.insert(segment_id, SegmentCtor::Point(ctor));
741                }
742                SegmentCtor::Line(ctor) => {
743                    final_edits.insert(segment_id, SegmentCtor::Line(ctor));
744                }
745                SegmentCtor::Arc(ctor) => {
746                    final_edits.insert(segment_id, SegmentCtor::Arc(ctor));
747                }
748                SegmentCtor::Circle(ctor) => {
749                    final_edits.insert(segment_id, SegmentCtor::Circle(ctor));
750                }
751            }
752        }
753
754        for (segment_id, ctor) in final_edits {
755            match ctor {
756                SegmentCtor::Point(ctor) => self
757                    .edit_point(&mut new_ast, sketch, segment_id, ctor)
758                    .map_err(KclErrorWithOutputs::no_outputs)?,
759                SegmentCtor::Line(ctor) => self
760                    .edit_line(&mut new_ast, sketch, segment_id, ctor)
761                    .map_err(KclErrorWithOutputs::no_outputs)?,
762                SegmentCtor::Arc(ctor) => self
763                    .edit_arc(&mut new_ast, sketch, segment_id, ctor)
764                    .map_err(KclErrorWithOutputs::no_outputs)?,
765                SegmentCtor::Circle(ctor) => self
766                    .edit_circle(&mut new_ast, sketch, segment_id, ctor)
767                    .map_err(KclErrorWithOutputs::no_outputs)?,
768            }
769        }
770        self.execute_after_edit(
771            ctx,
772            sketch,
773            sketch_block_ref,
774            segment_ids_edited,
775            EditDeleteKind::Edit,
776            &mut new_ast,
777        )
778        .await
779    }
780
781    async fn delete_objects(
782        &mut self,
783        ctx: &ExecutorContext,
784        _version: Version,
785        sketch: ObjectId,
786        constraint_ids: Vec<ObjectId>,
787        segment_ids: Vec<ObjectId>,
788    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
789        // TODO: Check version.
790        let sketch_block_ref =
791            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
792
793        // Deduplicate IDs.
794        let mut constraint_ids_set = constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
795        let segment_ids_set = segment_ids.into_iter().collect::<AhashIndexSet<_>>();
796
797        // If a point is owned by a Line/Arc, we want to delete the owner, which will
798        // also delete the point, as well as other points that are owned by the owner.
799        let mut resolved_segment_ids_to_delete = AhashIndexSet::default();
800
801        for segment_id in segment_ids_set.iter().copied() {
802            if let Some(segment_object) = self.scene_graph.objects.get(segment_id.0)
803                && let ObjectKind::Segment { segment } = &segment_object.kind
804                && let Segment::Point(point) = segment
805                && let Some(owner_id) = point.owner
806                && let Some(owner_object) = self.scene_graph.objects.get(owner_id.0)
807                && let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind
808                && matches!(owner_segment, Segment::Line(_) | Segment::Arc(_) | Segment::Circle(_))
809            {
810                // segment is owned -> delete the owner
811                resolved_segment_ids_to_delete.insert(owner_id);
812            } else {
813                // segment is not owned by anything -> can be deleted
814                resolved_segment_ids_to_delete.insert(segment_id);
815            }
816        }
817        let referenced_constraint_ids = self
818            .find_referenced_constraints(sketch, &resolved_segment_ids_to_delete)
819            .map_err(KclErrorWithOutputs::no_outputs)?;
820
821        let mut new_ast = self.program.ast.clone();
822
823        for constraint_id in referenced_constraint_ids {
824            if constraint_ids_set.contains(&constraint_id) {
825                continue;
826            }
827
828            let constraint_object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
829                KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Constraint not found: {constraint_id:?}")))
830            })?;
831            let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
832                return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
833                    "Object is not a constraint, it is {}",
834                    constraint_object.kind.human_friendly_kind_with_article()
835                ))));
836            };
837
838            match constraint {
839                Constraint::Coincident(coincident) => {
840                    let remaining_segments =
841                        self.remaining_constraint_segments(&coincident.segments, &resolved_segment_ids_to_delete);
842
843                    // If there are at least 2 segments left in the constraint: keep it, otherwise delete it.
844                    if remaining_segments.len() >= 2 {
845                        self.edit_coincident_constraint(&mut new_ast, constraint_id, remaining_segments)
846                            .map_err(KclErrorWithOutputs::no_outputs)?;
847                    } else {
848                        constraint_ids_set.insert(constraint_id);
849                    }
850                }
851                Constraint::EqualRadius(equal_radius) => {
852                    let remaining_input = equal_radius
853                        .input
854                        .iter()
855                        .copied()
856                        .filter(|segment_id| {
857                            !self.segment_will_be_deleted(*segment_id, &resolved_segment_ids_to_delete)
858                        })
859                        .collect::<Vec<_>>();
860
861                    if remaining_input.len() >= 2 {
862                        self.edit_equal_radius_constraint(&mut new_ast, constraint_id, remaining_input)
863                            .map_err(KclErrorWithOutputs::no_outputs)?;
864                    } else {
865                        constraint_ids_set.insert(constraint_id);
866                    }
867                }
868                Constraint::LinesEqualLength(lines_equal_length) => {
869                    let remaining_lines = lines_equal_length
870                        .lines
871                        .iter()
872                        .copied()
873                        .filter(|line_id| !self.segment_will_be_deleted(*line_id, &resolved_segment_ids_to_delete))
874                        .collect::<Vec<_>>();
875
876                    // Equal length constraint is only valid with at least 2 lines
877                    if remaining_lines.len() >= 2 {
878                        self.edit_equal_length_constraint(&mut new_ast, constraint_id, remaining_lines)
879                            .map_err(KclErrorWithOutputs::no_outputs)?;
880                    } else {
881                        constraint_ids_set.insert(constraint_id);
882                    }
883                }
884                Constraint::Parallel(parallel) => {
885                    let remaining_lines = parallel
886                        .lines
887                        .iter()
888                        .copied()
889                        .filter(|line_id| !self.segment_will_be_deleted(*line_id, &resolved_segment_ids_to_delete))
890                        .collect::<Vec<_>>();
891
892                    if remaining_lines.len() >= 2 {
893                        self.edit_parallel_constraint(&mut new_ast, constraint_id, remaining_lines)
894                            .map_err(KclErrorWithOutputs::no_outputs)?;
895                    } else {
896                        constraint_ids_set.insert(constraint_id);
897                    }
898                }
899                Constraint::Horizontal(Horizontal::Points { points }) => {
900                    let remaining_points = self.remaining_constraint_segments(points, &resolved_segment_ids_to_delete);
901
902                    if remaining_points.len() >= 2 {
903                        self.edit_horizontal_points_constraint(&mut new_ast, constraint_id, remaining_points)
904                            .map_err(KclErrorWithOutputs::no_outputs)?;
905                    } else {
906                        constraint_ids_set.insert(constraint_id);
907                    }
908                }
909                Constraint::Vertical(Vertical::Points { points }) => {
910                    let remaining_points = self.remaining_constraint_segments(points, &resolved_segment_ids_to_delete);
911
912                    if remaining_points.len() >= 2 {
913                        self.edit_vertical_points_constraint(&mut new_ast, constraint_id, remaining_points)
914                            .map_err(KclErrorWithOutputs::no_outputs)?;
915                    } else {
916                        constraint_ids_set.insert(constraint_id);
917                    }
918                }
919                Constraint::Fixed(fixed) => {
920                    if fixed.points.iter().any(|fixed_point| {
921                        self.segment_will_be_deleted(fixed_point.point, &resolved_segment_ids_to_delete)
922                    }) {
923                        constraint_ids_set.insert(constraint_id);
924                    }
925                }
926                _ => {
927                    // All other constraint types: if referenced by a segment -> delete the constraint
928                    constraint_ids_set.insert(constraint_id);
929                }
930            }
931        }
932
933        for constraint_id in constraint_ids_set {
934            self.delete_constraint(&mut new_ast, sketch, constraint_id)
935                .map_err(KclErrorWithOutputs::no_outputs)?;
936        }
937        for segment_id in resolved_segment_ids_to_delete {
938            self.delete_segment(&mut new_ast, sketch, segment_id)
939                .map_err(KclErrorWithOutputs::no_outputs)?;
940        }
941
942        self.execute_after_edit(
943            ctx,
944            sketch,
945            sketch_block_ref,
946            Default::default(),
947            EditDeleteKind::DeleteNonSketch,
948            &mut new_ast,
949        )
950        .await
951    }
952
953    async fn add_constraint(
954        &mut self,
955        ctx: &ExecutorContext,
956        _version: Version,
957        sketch: ObjectId,
958        constraint: Constraint,
959    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
960        // TODO: Check version.
961
962        // Save the original state as a backup - we'll restore it if anything fails
963        let original_program = self.program.clone();
964        let original_scene_graph = self.scene_graph.clone();
965
966        let mut new_ast = self.program.ast.clone();
967        let sketch_block_ref = match constraint {
968            Constraint::Coincident(coincident) => self
969                .add_coincident(sketch, coincident, &mut new_ast)
970                .await
971                .map_err(KclErrorWithOutputs::no_outputs)?,
972            Constraint::Distance(distance) => self
973                .add_distance(sketch, distance, &mut new_ast)
974                .await
975                .map_err(KclErrorWithOutputs::no_outputs)?,
976            Constraint::EqualRadius(equal_radius) => self
977                .add_equal_radius(sketch, equal_radius, &mut new_ast)
978                .await
979                .map_err(KclErrorWithOutputs::no_outputs)?,
980            Constraint::Fixed(fixed) => self
981                .add_fixed_constraints(sketch, fixed.points, &mut new_ast)
982                .await
983                .map_err(KclErrorWithOutputs::no_outputs)?,
984            Constraint::HorizontalDistance(distance) => self
985                .add_horizontal_distance(sketch, distance, &mut new_ast)
986                .await
987                .map_err(KclErrorWithOutputs::no_outputs)?,
988            Constraint::VerticalDistance(distance) => self
989                .add_vertical_distance(sketch, distance, &mut new_ast)
990                .await
991                .map_err(KclErrorWithOutputs::no_outputs)?,
992            Constraint::Horizontal(horizontal) => self
993                .add_horizontal(sketch, horizontal, &mut new_ast)
994                .await
995                .map_err(KclErrorWithOutputs::no_outputs)?,
996            Constraint::LinesEqualLength(lines_equal_length) => self
997                .add_lines_equal_length(sketch, lines_equal_length, &mut new_ast)
998                .await
999                .map_err(KclErrorWithOutputs::no_outputs)?,
1000            Constraint::Midpoint(midpoint) => self
1001                .add_midpoint(sketch, midpoint, &mut new_ast)
1002                .await
1003                .map_err(KclErrorWithOutputs::no_outputs)?,
1004            Constraint::Parallel(parallel) => self
1005                .add_parallel(sketch, parallel, &mut new_ast)
1006                .await
1007                .map_err(KclErrorWithOutputs::no_outputs)?,
1008            Constraint::Perpendicular(perpendicular) => self
1009                .add_perpendicular(sketch, perpendicular, &mut new_ast)
1010                .await
1011                .map_err(KclErrorWithOutputs::no_outputs)?,
1012            Constraint::Radius(radius) => self
1013                .add_radius(sketch, radius, &mut new_ast)
1014                .await
1015                .map_err(KclErrorWithOutputs::no_outputs)?,
1016            Constraint::Diameter(diameter) => self
1017                .add_diameter(sketch, diameter, &mut new_ast)
1018                .await
1019                .map_err(KclErrorWithOutputs::no_outputs)?,
1020            Constraint::Symmetric(symmetric) => self
1021                .add_symmetric(sketch, symmetric, &mut new_ast)
1022                .await
1023                .map_err(KclErrorWithOutputs::no_outputs)?,
1024            Constraint::Vertical(vertical) => self
1025                .add_vertical(sketch, vertical, &mut new_ast)
1026                .await
1027                .map_err(KclErrorWithOutputs::no_outputs)?,
1028            Constraint::Angle(lines_at_angle) => self
1029                .add_angle(sketch, lines_at_angle, &mut new_ast)
1030                .await
1031                .map_err(KclErrorWithOutputs::no_outputs)?,
1032            Constraint::Tangent(tangent) => self
1033                .add_tangent(sketch, tangent, &mut new_ast)
1034                .await
1035                .map_err(KclErrorWithOutputs::no_outputs)?,
1036        };
1037
1038        let result = self
1039            .execute_after_add_constraint(ctx, sketch, sketch_block_ref, &mut new_ast)
1040            .await;
1041
1042        // If execution failed, restore the original state to prevent corruption
1043        if result.is_err() {
1044            self.program = original_program;
1045            self.scene_graph = original_scene_graph;
1046        }
1047
1048        result
1049    }
1050
1051    async fn chain_segment(
1052        &mut self,
1053        ctx: &ExecutorContext,
1054        version: Version,
1055        sketch: ObjectId,
1056        previous_segment_end_point_id: ObjectId,
1057        segment: SegmentCtor,
1058        _label: Option<String>,
1059    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1060        // TODO: Check version.
1061
1062        // First, add the segment (line) to get its start point ID
1063        let SegmentCtor::Line(line_ctor) = segment else {
1064            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1065                "chain_segment currently only supports Line segments, got {}",
1066                segment.human_friendly_kind_with_article(),
1067            ))));
1068        };
1069
1070        // Add the line segment first - this updates self.program and self.scene_graph
1071        let (_first_src_delta, first_scene_delta) = self.add_line(ctx, sketch, line_ctor).await?;
1072
1073        // Find the new line's start point ID from the updated scene graph
1074        // add_line updates self.scene_graph, so we can use that
1075        let new_line_id = first_scene_delta
1076            .new_objects
1077            .iter()
1078            .find(|&obj_id| {
1079                let obj = self.scene_graph.objects.get(obj_id.0);
1080                if let Some(obj) = obj {
1081                    matches!(
1082                        &obj.kind,
1083                        ObjectKind::Segment {
1084                            segment: Segment::Line(_)
1085                        }
1086                    )
1087                } else {
1088                    false
1089                }
1090            })
1091            .ok_or_else(|| {
1092                KclErrorWithOutputs::no_outputs(KclError::refactor(
1093                    "Failed to find new line segment in scene graph".to_string(),
1094                ))
1095            })?;
1096
1097        let new_line_obj = self.scene_graph.objects.get(new_line_id.0).ok_or_else(|| {
1098            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1099                "New line object not found: {new_line_id:?}"
1100            )))
1101        })?;
1102
1103        let ObjectKind::Segment {
1104            segment: new_line_segment,
1105        } = &new_line_obj.kind
1106        else {
1107            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1108                "Object is not a segment: {new_line_obj:?}"
1109            ))));
1110        };
1111
1112        let Segment::Line(new_line) = new_line_segment else {
1113            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1114                "Segment is not a line: {new_line_segment:?}"
1115            ))));
1116        };
1117
1118        let new_line_start_point_id = new_line.start;
1119
1120        // Now add the coincident constraint between the previous end point and the new line's start point.
1121        let coincident = Coincident {
1122            segments: vec![previous_segment_end_point_id.into(), new_line_start_point_id.into()],
1123        };
1124
1125        let (final_src_delta, final_scene_delta) = self
1126            .add_constraint(ctx, version, sketch, Constraint::Coincident(coincident))
1127            .await?;
1128
1129        // Combine new objects from the line addition and the constraint addition.
1130        // Both add_line and add_constraint now populate new_objects correctly.
1131        let mut combined_new_objects = first_scene_delta.new_objects.clone();
1132        combined_new_objects.extend(final_scene_delta.new_objects);
1133
1134        let scene_graph_delta = SceneGraphDelta {
1135            new_graph: self.scene_graph.clone(),
1136            invalidates_ids: false,
1137            new_objects: combined_new_objects,
1138            exec_outcome: final_scene_delta.exec_outcome,
1139        };
1140
1141        Ok((final_src_delta, scene_graph_delta))
1142    }
1143
1144    async fn edit_constraint(
1145        &mut self,
1146        ctx: &ExecutorContext,
1147        _version: Version,
1148        sketch: ObjectId,
1149        constraint_id: ObjectId,
1150        value_expression: String,
1151    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1152        // TODO: Check version.
1153        let sketch_block_ref =
1154            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1155
1156        let object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
1157            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Object not found: {constraint_id:?}")))
1158        })?;
1159        if !matches!(&object.kind, ObjectKind::Constraint { .. }) {
1160            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1161                "Object is not a constraint: {constraint_id:?}"
1162            ))));
1163        }
1164
1165        let mut new_ast = self.program.ast.clone();
1166
1167        // Parse the expression string into an AST node.
1168        let (parsed, errors) = Program::parse(&value_expression)
1169            .map_err(|e| KclErrorWithOutputs::no_outputs(KclError::refactor(e.to_string())))?;
1170        if !errors.is_empty() {
1171            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1172                "Error parsing value expression: {errors:?}"
1173            ))));
1174        }
1175        let mut parsed = parsed.ok_or_else(|| {
1176            KclErrorWithOutputs::no_outputs(KclError::refactor("No AST produced from value expression".to_string()))
1177        })?;
1178        if parsed.ast.body.is_empty() {
1179            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1180                "Empty value expression".to_string(),
1181            )));
1182        }
1183        let first = parsed.ast.body.remove(0);
1184        let ast::BodyItem::ExpressionStatement(expr_stmt) = first else {
1185            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1186                "Value expression must be a simple expression".to_string(),
1187            )));
1188        };
1189
1190        let new_value: ast::BinaryPart = expr_stmt
1191            .inner
1192            .expression
1193            .try_into()
1194            .map_err(|e: String| KclErrorWithOutputs::no_outputs(KclError::refactor(e)))?;
1195
1196        self.mutate_ast(
1197            &mut new_ast,
1198            constraint_id,
1199            AstMutateCommand::EditConstraintValue { value: new_value },
1200        )
1201        .map_err(KclErrorWithOutputs::no_outputs)?;
1202
1203        self.execute_after_edit(
1204            ctx,
1205            sketch,
1206            sketch_block_ref,
1207            Default::default(),
1208            EditDeleteKind::Edit,
1209            &mut new_ast,
1210        )
1211        .await
1212    }
1213
1214    async fn edit_distance_constraint_label_position(
1215        &mut self,
1216        ctx: &ExecutorContext,
1217        _version: Version,
1218        sketch: ObjectId,
1219        constraint_id: ObjectId,
1220        label_position: Point2d<Number>,
1221        anchor_segment_ids: Vec<ObjectId>,
1222    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1223        // TODO: Check version.
1224        let sketch_block_ref =
1225            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1226
1227        let object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
1228            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Object not found: {constraint_id:?}")))
1229        })?;
1230        if !matches!(
1231            &object.kind,
1232            ObjectKind::Constraint {
1233                constraint: Constraint::Distance(_)
1234                    | Constraint::HorizontalDistance(_)
1235                    | Constraint::VerticalDistance(_),
1236            }
1237        ) {
1238            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1239                "Object is not a distance constraint: {constraint_id:?}"
1240            ))));
1241        }
1242
1243        let label_position = to_ast_point2d_number(&label_position).map_err(|err| {
1244            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1245                "Could not convert label position to AST: {err}"
1246            )))
1247        })?;
1248        let mut new_ast = self.program.ast.clone();
1249        self.mutate_ast(
1250            &mut new_ast,
1251            constraint_id,
1252            AstMutateCommand::EditDistanceConstraintLabelPosition { label_position },
1253        )
1254        .map_err(KclErrorWithOutputs::no_outputs)?;
1255
1256        self.execute_after_edit(
1257            ctx,
1258            sketch,
1259            sketch_block_ref,
1260            anchor_segment_ids.into_iter().collect(),
1261            EditDeleteKind::Edit,
1262            &mut new_ast,
1263        )
1264        .await
1265    }
1266
1267    /// Splitting a segment means creating a new segment, editing the old one, and then
1268    /// migrating a bunch of the constraints from the original segment to the new one
1269    /// (i.e. deleting them and re-adding them on the other segment).
1270    ///
1271    /// To keep this efficient we require as few executions as possible: we create the
1272    /// new segment first (to get its id), then do all edits and new constraints, and
1273    /// do all deletes at the end (since deletes invalidate ids).
1274    async fn batch_split_segment_operations(
1275        &mut self,
1276        ctx: &ExecutorContext,
1277        _version: Version,
1278        sketch: ObjectId,
1279        edit_segments: Vec<ExistingSegmentCtor>,
1280        add_constraints: Vec<Constraint>,
1281        delete_constraint_ids: Vec<ObjectId>,
1282        _new_segment_info: sketch::NewSegmentInfo,
1283    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1284        // TODO: Check version.
1285        let sketch_block_ref =
1286            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1287
1288        let mut new_ast = self.program.ast.clone();
1289        let mut segment_ids_edited = AhashIndexSet::with_capacity_and_hasher(edit_segments.len(), Default::default());
1290
1291        // Step 1: Edit segments
1292        for segment in edit_segments {
1293            segment_ids_edited.insert(segment.id);
1294            match segment.ctor {
1295                SegmentCtor::Point(ctor) => self
1296                    .edit_point(&mut new_ast, sketch, segment.id, ctor)
1297                    .map_err(KclErrorWithOutputs::no_outputs)?,
1298                SegmentCtor::Line(ctor) => self
1299                    .edit_line(&mut new_ast, sketch, segment.id, ctor)
1300                    .map_err(KclErrorWithOutputs::no_outputs)?,
1301                SegmentCtor::Arc(ctor) => self
1302                    .edit_arc(&mut new_ast, sketch, segment.id, ctor)
1303                    .map_err(KclErrorWithOutputs::no_outputs)?,
1304                SegmentCtor::Circle(ctor) => self
1305                    .edit_circle(&mut new_ast, sketch, segment.id, ctor)
1306                    .map_err(KclErrorWithOutputs::no_outputs)?,
1307            }
1308        }
1309
1310        // Step 2: Add all constraints
1311        for constraint in add_constraints {
1312            match constraint {
1313                Constraint::Coincident(coincident) => {
1314                    self.add_coincident(sketch, coincident, &mut new_ast)
1315                        .await
1316                        .map_err(KclErrorWithOutputs::no_outputs)?;
1317                }
1318                Constraint::Distance(distance) => {
1319                    self.add_distance(sketch, distance, &mut new_ast)
1320                        .await
1321                        .map_err(KclErrorWithOutputs::no_outputs)?;
1322                }
1323                Constraint::EqualRadius(equal_radius) => {
1324                    self.add_equal_radius(sketch, equal_radius, &mut new_ast)
1325                        .await
1326                        .map_err(KclErrorWithOutputs::no_outputs)?;
1327                }
1328                Constraint::Fixed(fixed) => {
1329                    self.add_fixed_constraints(sketch, fixed.points, &mut new_ast)
1330                        .await
1331                        .map_err(KclErrorWithOutputs::no_outputs)?;
1332                }
1333                Constraint::HorizontalDistance(distance) => {
1334                    self.add_horizontal_distance(sketch, distance, &mut new_ast)
1335                        .await
1336                        .map_err(KclErrorWithOutputs::no_outputs)?;
1337                }
1338                Constraint::VerticalDistance(distance) => {
1339                    self.add_vertical_distance(sketch, distance, &mut new_ast)
1340                        .await
1341                        .map_err(KclErrorWithOutputs::no_outputs)?;
1342                }
1343                Constraint::Horizontal(horizontal) => {
1344                    self.add_horizontal(sketch, horizontal, &mut new_ast)
1345                        .await
1346                        .map_err(KclErrorWithOutputs::no_outputs)?;
1347                }
1348                Constraint::LinesEqualLength(lines_equal_length) => {
1349                    self.add_lines_equal_length(sketch, lines_equal_length, &mut new_ast)
1350                        .await
1351                        .map_err(KclErrorWithOutputs::no_outputs)?;
1352                }
1353                Constraint::Midpoint(midpoint) => {
1354                    self.add_midpoint(sketch, midpoint, &mut new_ast)
1355                        .await
1356                        .map_err(KclErrorWithOutputs::no_outputs)?;
1357                }
1358                Constraint::Parallel(parallel) => {
1359                    self.add_parallel(sketch, parallel, &mut new_ast)
1360                        .await
1361                        .map_err(KclErrorWithOutputs::no_outputs)?;
1362                }
1363                Constraint::Perpendicular(perpendicular) => {
1364                    self.add_perpendicular(sketch, perpendicular, &mut new_ast)
1365                        .await
1366                        .map_err(KclErrorWithOutputs::no_outputs)?;
1367                }
1368                Constraint::Vertical(vertical) => {
1369                    self.add_vertical(sketch, vertical, &mut new_ast)
1370                        .await
1371                        .map_err(KclErrorWithOutputs::no_outputs)?;
1372                }
1373                Constraint::Diameter(diameter) => {
1374                    self.add_diameter(sketch, diameter, &mut new_ast)
1375                        .await
1376                        .map_err(KclErrorWithOutputs::no_outputs)?;
1377                }
1378                Constraint::Radius(radius) => {
1379                    self.add_radius(sketch, radius, &mut new_ast)
1380                        .await
1381                        .map_err(KclErrorWithOutputs::no_outputs)?;
1382                }
1383                Constraint::Symmetric(symmetric) => {
1384                    self.add_symmetric(sketch, symmetric, &mut new_ast)
1385                        .await
1386                        .map_err(KclErrorWithOutputs::no_outputs)?;
1387                }
1388                Constraint::Angle(angle) => {
1389                    self.add_angle(sketch, angle, &mut new_ast)
1390                        .await
1391                        .map_err(KclErrorWithOutputs::no_outputs)?;
1392                }
1393                Constraint::Tangent(tangent) => {
1394                    self.add_tangent(sketch, tangent, &mut new_ast)
1395                        .await
1396                        .map_err(KclErrorWithOutputs::no_outputs)?;
1397                }
1398            }
1399        }
1400
1401        // Step 3: Delete constraints (must be last since deletes can invalidate IDs)
1402        let constraint_ids_set = delete_constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
1403
1404        let has_constraint_deletions = !constraint_ids_set.is_empty();
1405        for constraint_id in constraint_ids_set {
1406            self.delete_constraint(&mut new_ast, sketch, constraint_id)
1407                .map_err(KclErrorWithOutputs::no_outputs)?;
1408        }
1409
1410        // Step 4: Execute once at the end
1411        // Always use Edit (not DeleteNonSketch) because we're editing the sketch block, not deleting it
1412        // But we'll manually set invalidates_ids: true if we deleted constraints
1413        let (source_delta, mut scene_graph_delta) = self
1414            .execute_after_edit(
1415                ctx,
1416                sketch,
1417                sketch_block_ref,
1418                segment_ids_edited,
1419                EditDeleteKind::Edit,
1420                &mut new_ast,
1421            )
1422            .await?;
1423
1424        // If we deleted constraints, set invalidates_ids: true
1425        // This is because constraint deletion invalidates IDs, even though we're not deleting the sketch block
1426        if has_constraint_deletions {
1427            scene_graph_delta.invalidates_ids = true;
1428        }
1429
1430        Ok((source_delta, scene_graph_delta))
1431    }
1432
1433    async fn batch_tail_cut_operations(
1434        &mut self,
1435        ctx: &ExecutorContext,
1436        _version: Version,
1437        sketch: ObjectId,
1438        edit_segments: Vec<ExistingSegmentCtor>,
1439        add_constraints: Vec<Constraint>,
1440        delete_constraint_ids: Vec<ObjectId>,
1441    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1442        let sketch_block_ref =
1443            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1444
1445        let mut new_ast = self.program.ast.clone();
1446        let mut segment_ids_edited = AhashIndexSet::with_capacity_and_hasher(edit_segments.len(), Default::default());
1447
1448        // Step 1: Edit segments (usually a single segment for tail cut)
1449        for segment in edit_segments {
1450            segment_ids_edited.insert(segment.id);
1451            match segment.ctor {
1452                SegmentCtor::Point(ctor) => self
1453                    .edit_point(&mut new_ast, sketch, segment.id, ctor)
1454                    .map_err(KclErrorWithOutputs::no_outputs)?,
1455                SegmentCtor::Line(ctor) => self
1456                    .edit_line(&mut new_ast, sketch, segment.id, ctor)
1457                    .map_err(KclErrorWithOutputs::no_outputs)?,
1458                SegmentCtor::Arc(ctor) => self
1459                    .edit_arc(&mut new_ast, sketch, segment.id, ctor)
1460                    .map_err(KclErrorWithOutputs::no_outputs)?,
1461                SegmentCtor::Circle(ctor) => self
1462                    .edit_circle(&mut new_ast, sketch, segment.id, ctor)
1463                    .map_err(KclErrorWithOutputs::no_outputs)?,
1464            }
1465        }
1466
1467        // Step 2: Add coincident constraints
1468        for constraint in add_constraints {
1469            match constraint {
1470                Constraint::Coincident(coincident) => {
1471                    self.add_coincident(sketch, coincident, &mut new_ast)
1472                        .await
1473                        .map_err(KclErrorWithOutputs::no_outputs)?;
1474                }
1475                other => {
1476                    return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1477                        "unsupported constraint in tail cut batch: {other:?}"
1478                    ))));
1479                }
1480            }
1481        }
1482
1483        // Step 3: Delete constraints (if any)
1484        let constraint_ids_set = delete_constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
1485
1486        let has_constraint_deletions = !constraint_ids_set.is_empty();
1487        for constraint_id in constraint_ids_set {
1488            self.delete_constraint(&mut new_ast, sketch, constraint_id)
1489                .map_err(KclErrorWithOutputs::no_outputs)?;
1490        }
1491
1492        // Step 4: Single execute_after_edit
1493        // Always use Edit (not DeleteNonSketch) because we're editing the sketch block, not deleting it
1494        // But we'll manually set invalidates_ids: true if we deleted constraints
1495        let (source_delta, mut scene_graph_delta) = self
1496            .execute_after_edit(
1497                ctx,
1498                sketch,
1499                sketch_block_ref,
1500                segment_ids_edited,
1501                EditDeleteKind::Edit,
1502                &mut new_ast,
1503            )
1504            .await?;
1505
1506        // If we deleted constraints, set invalidates_ids: true
1507        // This is because constraint deletion invalidates IDs, even though we're not deleting the sketch block
1508        if has_constraint_deletions {
1509            scene_graph_delta.invalidates_ids = true;
1510        }
1511
1512        Ok((source_delta, scene_graph_delta))
1513    }
1514}
1515
1516impl FrontendState {
1517    pub async fn hack_set_program(&mut self, ctx: &ExecutorContext, program: Program) -> ExecResult<SetProgramOutcome> {
1518        self.program = program.clone();
1519
1520        // Execute so that the objects are updated and available for the next
1521        // API call.
1522        // This always uses engine execution (not mock) so that things are cached.
1523        // Engine execution now runs freedom analysis automatically.
1524        // Keep existing checkpoints alive here. History may still reference
1525        // older committed sketch states across a direct-edit boundary, and a
1526        // checkpoint restore is a full state replacement anyway. We append a
1527        // fresh baseline checkpoint after the full execution below.
1528        // Clear the freedom cache since IDs might have changed after direct editing
1529        // and we're about to run freedom analysis which will repopulate it.
1530        self.point_freedom_cache.clear();
1531        match ctx.run_with_caching(program).await {
1532            Ok(outcome) => {
1533                let outcome = self.update_state_after_exec(outcome, true);
1534                let checkpoint_id = self
1535                    .create_sketch_checkpoint(outcome.clone())
1536                    .await
1537                    .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
1538                Ok(SetProgramOutcome::Success {
1539                    scene_graph: Box::new(self.scene_graph.clone()),
1540                    exec_outcome: Box::new(outcome),
1541                    checkpoint_id: Some(checkpoint_id),
1542                })
1543            }
1544            Err(mut err) => {
1545                // Don't return an error just because execution failed. Instead,
1546                // update state as much as possible.
1547                let outcome = self.exec_outcome_from_exec_error(err.clone())?;
1548                self.update_state_after_exec(outcome, true);
1549                err.scene_graph = Some(self.scene_graph.clone());
1550                Ok(SetProgramOutcome::ExecFailure { error: Box::new(err) })
1551            }
1552        }
1553    }
1554
1555    /// Decorate engine execution such that our state is updated and the scene
1556    /// graph is added to the return.
1557    pub async fn engine_execute(
1558        &mut self,
1559        ctx: &ExecutorContext,
1560        program: Program,
1561    ) -> Result<SceneGraphDelta, KclErrorWithOutputs> {
1562        self.program = program.clone();
1563
1564        // Engine execution now runs freedom analysis automatically. Clear the
1565        // freedom cache since IDs might have changed after direct editing, and
1566        // we're about to run freedom analysis which will repopulate it.
1567        self.point_freedom_cache.clear();
1568        match ctx.run_with_caching(program).await {
1569            Ok(outcome) => {
1570                let outcome = self.update_state_after_exec(outcome, true);
1571                Ok(SceneGraphDelta {
1572                    new_graph: self.scene_graph.clone(),
1573                    exec_outcome: outcome,
1574                    // We don't know what the new objects are.
1575                    new_objects: Default::default(),
1576                    // We don't know if IDs were invalidated.
1577                    invalidates_ids: Default::default(),
1578                })
1579            }
1580            Err(mut err) => {
1581                // Update state as much as possible, even when there's an error.
1582                let outcome = self.exec_outcome_from_exec_error(err.clone())?;
1583                self.update_state_after_exec(outcome, true);
1584                err.scene_graph = Some(self.scene_graph.clone());
1585                Err(err)
1586            }
1587        }
1588    }
1589
1590    fn exec_outcome_from_exec_error(&self, err: KclErrorWithOutputs) -> Result<ExecOutcome, KclErrorWithOutputs> {
1591        if matches!(err.error, KclError::EngineHangup { .. }) {
1592            // It's not ideal to special-case this, but this error is very
1593            // common during development, and it causes confusing downstream
1594            // errors that have nothing to do with the actual problem.
1595            return Err(err);
1596        }
1597
1598        let KclErrorWithOutputs {
1599            error,
1600            mut non_fatal,
1601            variables,
1602            #[cfg(feature = "artifact-graph")]
1603            operations,
1604            #[cfg(feature = "artifact-graph")]
1605            artifact_graph,
1606            #[cfg(feature = "artifact-graph")]
1607            scene_objects,
1608            #[cfg(feature = "artifact-graph")]
1609            source_range_to_object,
1610            #[cfg(feature = "artifact-graph")]
1611            var_solutions,
1612            filenames,
1613            default_planes,
1614            ..
1615        } = err;
1616
1617        if let Some(source_range) = error.source_ranges().first() {
1618            non_fatal.push(CompilationIssue::fatal(*source_range, error.get_message()));
1619        } else {
1620            non_fatal.push(CompilationIssue::fatal(SourceRange::synthetic(), error.get_message()));
1621        }
1622
1623        Ok(ExecOutcome {
1624            variables,
1625            filenames,
1626            #[cfg(feature = "artifact-graph")]
1627            operations,
1628            #[cfg(feature = "artifact-graph")]
1629            artifact_graph,
1630            #[cfg(feature = "artifact-graph")]
1631            scene_objects,
1632            #[cfg(feature = "artifact-graph")]
1633            source_range_to_object,
1634            #[cfg(feature = "artifact-graph")]
1635            var_solutions,
1636            issues: non_fatal,
1637            default_planes,
1638        })
1639    }
1640
1641    async fn add_point(
1642        &mut self,
1643        ctx: &ExecutorContext,
1644        sketch: ObjectId,
1645        ctor: PointCtor,
1646    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1647        // Create updated KCL source from args.
1648        let at_ast = to_ast_point2d(&ctor.position)
1649            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1650        let point_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
1651            callee: ast::Node::no_src(ast_sketch2_name(POINT_FN)),
1652            unlabeled: None,
1653            arguments: vec![ast::LabeledArg {
1654                label: Some(ast::Identifier::new(POINT_AT_PARAM)),
1655                arg: at_ast,
1656            }],
1657            digest: None,
1658            non_code_meta: Default::default(),
1659        })));
1660
1661        // Look up existing sketch.
1662        let sketch_id = sketch;
1663        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
1664            #[cfg(target_arch = "wasm32")]
1665            web_sys::console::error_1(
1666                &format!(
1667                    "Sketch not found; sketch_id={sketch_id:?}, self.scene_graph.objects={:#?}",
1668                    &self.scene_graph.objects
1669                )
1670                .into(),
1671            );
1672            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
1673        })?;
1674        let ObjectKind::Sketch(_) = &sketch_object.kind else {
1675            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1676                "Object is not a sketch, it is {}",
1677                sketch_object.kind.human_friendly_kind_with_article(),
1678            ))));
1679        };
1680        // Add the point to the AST of the sketch block.
1681        let mut new_ast = self.program.ast.clone();
1682        let (sketch_block_ref, _) = self
1683            .mutate_ast(
1684                &mut new_ast,
1685                sketch_id,
1686                AstMutateCommand::AddSketchBlockExprStmt { expr: point_ast },
1687            )
1688            .map_err(KclErrorWithOutputs::no_outputs)?;
1689        // Convert to string source to create real source ranges.
1690        let new_source = source_from_ast(&new_ast);
1691        // Parse the new KCL source.
1692        let (new_program, errors) = Program::parse(&new_source)
1693            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1694        if !errors.is_empty() {
1695            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1696                "Error parsing KCL source after adding point: {errors:?}"
1697            ))));
1698        }
1699        let Some(new_program) = new_program else {
1700            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1701                "No AST produced after adding point".to_string(),
1702            )));
1703        };
1704
1705        let point_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
1706            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1707                "Source range of point not found in sketch block: {sketch_block_ref:?}; {err:?}"
1708            )))
1709        })?;
1710        #[cfg(not(feature = "artifact-graph"))]
1711        let _ = point_node_ref;
1712
1713        // Make sure to only set this if there are no errors.
1714        self.program = new_program.clone();
1715
1716        // Truncate after the sketch block for mock execution.
1717        let mut truncated_program = new_program;
1718        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
1719            .map_err(KclErrorWithOutputs::no_outputs)?;
1720
1721        // Execute.
1722        let outcome = ctx
1723            .run_mock(
1724                &truncated_program,
1725                &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
1726            )
1727            .await?;
1728
1729        #[cfg(not(feature = "artifact-graph"))]
1730        let new_object_ids = Vec::new();
1731        #[cfg(feature = "artifact-graph")]
1732        let new_object_ids = {
1733            let make_err =
1734                |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
1735            let segment_id = outcome
1736                .source_range_to_object
1737                .get(&point_node_ref.range)
1738                .copied()
1739                .ok_or_else(|| make_err(format!("Source range of point not found: {point_node_ref:?}")))?;
1740            let segment_object = outcome
1741                .scene_objects
1742                .get(segment_id.0)
1743                .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
1744            let ObjectKind::Segment { segment } = &segment_object.kind else {
1745                return Err(make_err(format!(
1746                    "Object is not a segment, it is {}",
1747                    segment_object.kind.human_friendly_kind_with_article()
1748                )));
1749            };
1750            let Segment::Point(_) = segment else {
1751                return Err(make_err(format!(
1752                    "Segment is not a point, it is {}",
1753                    segment.human_friendly_kind_with_article()
1754                )));
1755            };
1756            vec![segment_id]
1757        };
1758        let src_delta = SourceDelta { text: new_source };
1759        // Uses .no_freedom_analysis() so freedom_analysis: false
1760        let outcome = self.update_state_after_exec(outcome, false);
1761        let scene_graph_delta = SceneGraphDelta {
1762            new_graph: self.scene_graph.clone(),
1763            invalidates_ids: false,
1764            new_objects: new_object_ids,
1765            exec_outcome: outcome,
1766        };
1767        Ok((src_delta, scene_graph_delta))
1768    }
1769
1770    async fn add_line(
1771        &mut self,
1772        ctx: &ExecutorContext,
1773        sketch: ObjectId,
1774        ctor: LineCtor,
1775    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1776        // Create updated KCL source from args.
1777        let start_ast = to_ast_point2d(&ctor.start)
1778            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1779        let end_ast = to_ast_point2d(&ctor.end)
1780            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1781        let mut arguments = vec![
1782            ast::LabeledArg {
1783                label: Some(ast::Identifier::new(LINE_START_PARAM)),
1784                arg: start_ast,
1785            },
1786            ast::LabeledArg {
1787                label: Some(ast::Identifier::new(LINE_END_PARAM)),
1788                arg: end_ast,
1789            },
1790        ];
1791        // Add construction kwarg if construction is Some(true)
1792        if ctor.construction == Some(true) {
1793            arguments.push(ast::LabeledArg {
1794                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
1795                arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
1796                    value: ast::LiteralValue::Bool(true),
1797                    raw: "true".to_string(),
1798                    digest: None,
1799                }))),
1800            });
1801        }
1802        let line_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
1803            callee: ast::Node::no_src(ast_sketch2_name(LINE_FN)),
1804            unlabeled: None,
1805            arguments,
1806            digest: None,
1807            non_code_meta: Default::default(),
1808        })));
1809
1810        // Look up existing sketch.
1811        let sketch_id = sketch;
1812        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
1813            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
1814        })?;
1815        let ObjectKind::Sketch(_) = &sketch_object.kind else {
1816            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1817                "Object is not a sketch, it is {}",
1818                sketch_object.kind.human_friendly_kind_with_article(),
1819            ))));
1820        };
1821        // Add the line to the AST of the sketch block.
1822        let mut new_ast = self.program.ast.clone();
1823        let (sketch_block_ref, _) = self
1824            .mutate_ast(
1825                &mut new_ast,
1826                sketch_id,
1827                AstMutateCommand::AddSketchBlockExprStmt { expr: line_ast },
1828            )
1829            .map_err(KclErrorWithOutputs::no_outputs)?;
1830        // Convert to string source to create real source ranges.
1831        let new_source = source_from_ast(&new_ast);
1832        // Parse the new KCL source.
1833        let (new_program, errors) = Program::parse(&new_source)
1834            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1835        if !errors.is_empty() {
1836            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1837                "Error parsing KCL source after adding line: {errors:?}"
1838            ))));
1839        }
1840        let Some(new_program) = new_program else {
1841            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1842                "No AST produced after adding line".to_string(),
1843            )));
1844        };
1845
1846        let line_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
1847            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1848                "Source range of line not found in sketch block: {sketch_block_ref:?}; {err:?}"
1849            )))
1850        })?;
1851        #[cfg(not(feature = "artifact-graph"))]
1852        let _ = line_node_ref;
1853
1854        // Make sure to only set this if there are no errors.
1855        self.program = new_program.clone();
1856
1857        // Truncate after the sketch block for mock execution.
1858        let mut truncated_program = new_program;
1859        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
1860            .map_err(KclErrorWithOutputs::no_outputs)?;
1861
1862        // Execute.
1863        let outcome = ctx
1864            .run_mock(
1865                &truncated_program,
1866                &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
1867            )
1868            .await?;
1869
1870        #[cfg(not(feature = "artifact-graph"))]
1871        let new_object_ids = Vec::new();
1872        #[cfg(feature = "artifact-graph")]
1873        let new_object_ids = {
1874            let make_err =
1875                |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
1876            let segment_id = outcome
1877                .source_range_to_object
1878                .get(&line_node_ref.range)
1879                .copied()
1880                .ok_or_else(|| make_err(format!("Source range of line not found: {line_node_ref:?}")))?;
1881            let segment_object = outcome
1882                .scene_object_by_id(segment_id)
1883                .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
1884            let ObjectKind::Segment { segment } = &segment_object.kind else {
1885                return Err(make_err(format!(
1886                    "Object is not a segment, it is {}",
1887                    segment_object.kind.human_friendly_kind_with_article()
1888                )));
1889            };
1890            let Segment::Line(line) = segment else {
1891                return Err(make_err(format!(
1892                    "Segment is not a line, it is {}",
1893                    segment.human_friendly_kind_with_article()
1894                )));
1895            };
1896            vec![line.start, line.end, segment_id]
1897        };
1898        let src_delta = SourceDelta { text: new_source };
1899        // Uses .no_freedom_analysis() so freedom_analysis: false
1900        let outcome = self.update_state_after_exec(outcome, false);
1901        let scene_graph_delta = SceneGraphDelta {
1902            new_graph: self.scene_graph.clone(),
1903            invalidates_ids: false,
1904            new_objects: new_object_ids,
1905            exec_outcome: outcome,
1906        };
1907        Ok((src_delta, scene_graph_delta))
1908    }
1909
1910    async fn add_arc(
1911        &mut self,
1912        ctx: &ExecutorContext,
1913        sketch: ObjectId,
1914        ctor: ArcCtor,
1915    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1916        // Create updated KCL source from args.
1917        let start_ast = to_ast_point2d(&ctor.start)
1918            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1919        let end_ast = to_ast_point2d(&ctor.end)
1920            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1921        let center_ast = to_ast_point2d(&ctor.center)
1922            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1923        let mut arguments = vec![
1924            ast::LabeledArg {
1925                label: Some(ast::Identifier::new(ARC_START_PARAM)),
1926                arg: start_ast,
1927            },
1928            ast::LabeledArg {
1929                label: Some(ast::Identifier::new(ARC_END_PARAM)),
1930                arg: end_ast,
1931            },
1932            ast::LabeledArg {
1933                label: Some(ast::Identifier::new(ARC_CENTER_PARAM)),
1934                arg: center_ast,
1935            },
1936        ];
1937        // Add construction kwarg if construction is Some(true)
1938        if ctor.construction == Some(true) {
1939            arguments.push(ast::LabeledArg {
1940                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
1941                arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
1942                    value: ast::LiteralValue::Bool(true),
1943                    raw: "true".to_string(),
1944                    digest: None,
1945                }))),
1946            });
1947        }
1948        let arc_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
1949            callee: ast::Node::no_src(ast_sketch2_name(ARC_FN)),
1950            unlabeled: None,
1951            arguments,
1952            digest: None,
1953            non_code_meta: Default::default(),
1954        })));
1955
1956        // Look up existing sketch.
1957        let sketch_id = sketch;
1958        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
1959            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
1960        })?;
1961        let ObjectKind::Sketch(_) = &sketch_object.kind else {
1962            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1963                "Object is not a sketch, it is {}",
1964                sketch_object.kind.human_friendly_kind_with_article(),
1965            ))));
1966        };
1967        // Add the arc to the AST of the sketch block.
1968        let mut new_ast = self.program.ast.clone();
1969        let (sketch_block_ref, _) = self
1970            .mutate_ast(
1971                &mut new_ast,
1972                sketch_id,
1973                AstMutateCommand::AddSketchBlockExprStmt { expr: arc_ast },
1974            )
1975            .map_err(KclErrorWithOutputs::no_outputs)?;
1976        // Convert to string source to create real source ranges.
1977        let new_source = source_from_ast(&new_ast);
1978        // Parse the new KCL source.
1979        let (new_program, errors) = Program::parse(&new_source)
1980            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1981        if !errors.is_empty() {
1982            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1983                "Error parsing KCL source after adding arc: {errors:?}"
1984            ))));
1985        }
1986        let Some(new_program) = new_program else {
1987            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1988                "No AST produced after adding arc".to_string(),
1989            )));
1990        };
1991
1992        let arc_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
1993            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1994                "Source range of arc not found in sketch block: {sketch_block_ref:?}; {err:?}"
1995            )))
1996        })?;
1997        #[cfg(not(feature = "artifact-graph"))]
1998        let _ = arc_node_ref;
1999
2000        // Make sure to only set this if there are no errors.
2001        self.program = new_program.clone();
2002
2003        // Truncate after the sketch block for mock execution.
2004        let mut truncated_program = new_program;
2005        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2006            .map_err(KclErrorWithOutputs::no_outputs)?;
2007
2008        // Execute.
2009        let outcome = ctx
2010            .run_mock(
2011                &truncated_program,
2012                &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2013            )
2014            .await?;
2015
2016        #[cfg(not(feature = "artifact-graph"))]
2017        let new_object_ids = Vec::new();
2018        #[cfg(feature = "artifact-graph")]
2019        let new_object_ids = {
2020            let make_err =
2021                |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2022            let segment_id = outcome
2023                .source_range_to_object
2024                .get(&arc_node_ref.range)
2025                .copied()
2026                .ok_or_else(|| make_err(format!("Source range of arc not found: {arc_node_ref:?}")))?;
2027            let segment_object = outcome
2028                .scene_objects
2029                .get(segment_id.0)
2030                .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2031            let ObjectKind::Segment { segment } = &segment_object.kind else {
2032                return Err(make_err(format!(
2033                    "Object is not a segment, it is {}",
2034                    segment_object.kind.human_friendly_kind_with_article()
2035                )));
2036            };
2037            let Segment::Arc(arc) = segment else {
2038                return Err(make_err(format!(
2039                    "Segment is not an arc, it is {}",
2040                    segment.human_friendly_kind_with_article()
2041                )));
2042            };
2043            vec![arc.start, arc.end, arc.center, segment_id]
2044        };
2045        let src_delta = SourceDelta { text: new_source };
2046        // Uses .no_freedom_analysis() so freedom_analysis: false
2047        let outcome = self.update_state_after_exec(outcome, false);
2048        let scene_graph_delta = SceneGraphDelta {
2049            new_graph: self.scene_graph.clone(),
2050            invalidates_ids: false,
2051            new_objects: new_object_ids,
2052            exec_outcome: outcome,
2053        };
2054        Ok((src_delta, scene_graph_delta))
2055    }
2056
2057    async fn add_circle(
2058        &mut self,
2059        ctx: &ExecutorContext,
2060        sketch: ObjectId,
2061        ctor: CircleCtor,
2062    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2063        // Create updated KCL source from args.
2064        let start_ast = to_ast_point2d(&ctor.start)
2065            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2066        let center_ast = to_ast_point2d(&ctor.center)
2067            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2068        let mut arguments = vec![
2069            ast::LabeledArg {
2070                label: Some(ast::Identifier::new(CIRCLE_START_PARAM)),
2071                arg: start_ast,
2072            },
2073            ast::LabeledArg {
2074                label: Some(ast::Identifier::new(CIRCLE_CENTER_PARAM)),
2075                arg: center_ast,
2076            },
2077        ];
2078        // Add construction kwarg if construction is Some(true)
2079        if ctor.construction == Some(true) {
2080            arguments.push(ast::LabeledArg {
2081                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2082                arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
2083                    value: ast::LiteralValue::Bool(true),
2084                    raw: "true".to_string(),
2085                    digest: None,
2086                }))),
2087            });
2088        }
2089        let circle_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
2090            callee: ast::Node::no_src(ast_sketch2_name(CIRCLE_FN)),
2091            unlabeled: None,
2092            arguments,
2093            digest: None,
2094            non_code_meta: Default::default(),
2095        })));
2096
2097        // Look up existing sketch.
2098        let sketch_id = sketch;
2099        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2100            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2101        })?;
2102        let ObjectKind::Sketch(_) = &sketch_object.kind else {
2103            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2104                "Object is not a sketch, it is {}",
2105                sketch_object.kind.human_friendly_kind_with_article(),
2106            ))));
2107        };
2108        // Add the circle to the AST of the sketch block.
2109        let mut new_ast = self.program.ast.clone();
2110        let (sketch_block_ref, _) = self
2111            .mutate_ast(
2112                &mut new_ast,
2113                sketch_id,
2114                AstMutateCommand::AddSketchBlockVarDecl {
2115                    prefix: CIRCLE_VARIABLE.to_owned(),
2116                    expr: circle_ast,
2117                },
2118            )
2119            .map_err(KclErrorWithOutputs::no_outputs)?;
2120        // Convert to string source to create real source ranges.
2121        let new_source = source_from_ast(&new_ast);
2122        // Parse the new KCL source.
2123        let (new_program, errors) = Program::parse(&new_source)
2124            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2125        if !errors.is_empty() {
2126            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2127                "Error parsing KCL source after adding circle: {errors:?}"
2128            ))));
2129        }
2130        let Some(new_program) = new_program else {
2131            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
2132                "No AST produced after adding circle".to_string(),
2133            )));
2134        };
2135
2136        let circle_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2137            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2138                "Source range of circle not found in sketch block: {sketch_block_ref:?}; {err:?}"
2139            )))
2140        })?;
2141        #[cfg(not(feature = "artifact-graph"))]
2142        let _ = circle_node_ref;
2143
2144        // Make sure to only set this if there are no errors.
2145        self.program = new_program.clone();
2146
2147        // Truncate after the sketch block for mock execution.
2148        let mut truncated_program = new_program;
2149        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2150            .map_err(KclErrorWithOutputs::no_outputs)?;
2151
2152        // Execute.
2153        let outcome = ctx
2154            .run_mock(
2155                &truncated_program,
2156                &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2157            )
2158            .await?;
2159
2160        #[cfg(not(feature = "artifact-graph"))]
2161        let new_object_ids = Vec::new();
2162        #[cfg(feature = "artifact-graph")]
2163        let new_object_ids = {
2164            let make_err =
2165                |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2166            let segment_id = outcome
2167                .source_range_to_object
2168                .get(&circle_node_ref.range)
2169                .copied()
2170                .ok_or_else(|| make_err(format!("Source range of circle not found: {circle_node_ref:?}")))?;
2171            let segment_object = outcome
2172                .scene_objects
2173                .get(segment_id.0)
2174                .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2175            let ObjectKind::Segment { segment } = &segment_object.kind else {
2176                return Err(make_err(format!(
2177                    "Object is not a segment, it is {}",
2178                    segment_object.kind.human_friendly_kind_with_article()
2179                )));
2180            };
2181            let Segment::Circle(circle) = segment else {
2182                return Err(make_err(format!(
2183                    "Segment is not a circle, it is {}",
2184                    segment.human_friendly_kind_with_article()
2185                )));
2186            };
2187            vec![circle.start, circle.center, segment_id]
2188        };
2189        let src_delta = SourceDelta { text: new_source };
2190        // Uses .no_freedom_analysis() so freedom_analysis: false
2191        let outcome = self.update_state_after_exec(outcome, false);
2192        let scene_graph_delta = SceneGraphDelta {
2193            new_graph: self.scene_graph.clone(),
2194            invalidates_ids: false,
2195            new_objects: new_object_ids,
2196            exec_outcome: outcome,
2197        };
2198        Ok((src_delta, scene_graph_delta))
2199    }
2200
2201    fn edit_point(
2202        &mut self,
2203        new_ast: &mut ast::Node<ast::Program>,
2204        sketch: ObjectId,
2205        point: ObjectId,
2206        ctor: PointCtor,
2207    ) -> Result<(), KclError> {
2208        // Create updated KCL source from args.
2209        let new_at_ast = to_ast_point2d(&ctor.position).map_err(|err| KclError::refactor(err.to_string()))?;
2210
2211        // Look up existing sketch.
2212        let sketch_id = sketch;
2213        let sketch_object = self
2214            .scene_graph
2215            .objects
2216            .get(sketch_id.0)
2217            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2218        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2219            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2220        };
2221        sketch.segments.iter().find(|o| **o == point).ok_or_else(|| {
2222            KclError::refactor(format!("Point not found in sketch: point={point:?}, sketch={sketch:?}"))
2223        })?;
2224        // Look up existing point.
2225        let point_id = point;
2226        let point_object = self
2227            .scene_graph
2228            .objects
2229            .get(point_id.0)
2230            .ok_or_else(|| KclError::refactor(format!("Point not found in scene graph: point={point:?}")))?;
2231        let ObjectKind::Segment {
2232            segment: Segment::Point(point),
2233        } = &point_object.kind
2234        else {
2235            return Err(KclError::refactor(format!(
2236                "Object is not a point segment: {point_object:?}"
2237            )));
2238        };
2239
2240        // If the point is part of a line or arc, edit the line/arc instead.
2241        if let Some(owner_id) = point.owner {
2242            let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
2243                KclError::refactor(format!(
2244                    "Internal: Owner of point not found in scene graph: owner={owner_id:?}",
2245                ))
2246            })?;
2247            let ObjectKind::Segment { segment } = &owner_object.kind else {
2248                return Err(KclError::refactor(format!(
2249                    "Internal: Owner of point is not a segment, but found {}",
2250                    owner_object.kind.human_friendly_kind_with_article()
2251                )));
2252            };
2253
2254            // Handle Line owner
2255            if let Segment::Line(line) = segment {
2256                let SegmentCtor::Line(line_ctor) = &line.ctor else {
2257                    return Err(KclError::refactor(format!(
2258                        "Internal: Owner of point does not have line ctor, but found {}",
2259                        line.ctor.human_friendly_kind_with_article()
2260                    )));
2261                };
2262                let mut line_ctor = line_ctor.clone();
2263                // Which end of the line is this point?
2264                if line.start == point_id {
2265                    line_ctor.start = ctor.position;
2266                } else if line.end == point_id {
2267                    line_ctor.end = ctor.position;
2268                } else {
2269                    return Err(KclError::refactor(format!(
2270                        "Internal: Point is not part of owner's line segment: point={point_id:?}, line={owner_id:?}"
2271                    )));
2272                }
2273                return self.edit_line(new_ast, sketch_id, owner_id, line_ctor);
2274            }
2275
2276            // Handle Arc owner
2277            if let Segment::Arc(arc) = segment {
2278                let SegmentCtor::Arc(arc_ctor) = &arc.ctor else {
2279                    return Err(KclError::refactor(format!(
2280                        "Internal: Owner of point does not have arc ctor, but found {}",
2281                        arc.ctor.human_friendly_kind_with_article()
2282                    )));
2283                };
2284                let mut arc_ctor = arc_ctor.clone();
2285                // Which point of the arc is this? (center, start, or end)
2286                if arc.center == point_id {
2287                    arc_ctor.center = ctor.position;
2288                } else if arc.start == point_id {
2289                    arc_ctor.start = ctor.position;
2290                } else if arc.end == point_id {
2291                    arc_ctor.end = ctor.position;
2292                } else {
2293                    return Err(KclError::refactor(format!(
2294                        "Internal: Point is not part of owner's arc segment: point={point_id:?}, arc={owner_id:?}"
2295                    )));
2296                }
2297                return self.edit_arc(new_ast, sketch_id, owner_id, arc_ctor);
2298            }
2299
2300            // Handle Circle owner
2301            if let Segment::Circle(circle) = segment {
2302                let SegmentCtor::Circle(circle_ctor) = &circle.ctor else {
2303                    return Err(KclError::refactor(format!(
2304                        "Internal: Owner of point does not have circle ctor, but found {}",
2305                        circle.ctor.human_friendly_kind_with_article()
2306                    )));
2307                };
2308                let mut circle_ctor = circle_ctor.clone();
2309                if circle.center == point_id {
2310                    circle_ctor.center = ctor.position;
2311                } else if circle.start == point_id {
2312                    circle_ctor.start = ctor.position;
2313                } else {
2314                    return Err(KclError::refactor(format!(
2315                        "Internal: Point is not part of owner's circle segment: point={point_id:?}, circle={owner_id:?}"
2316                    )));
2317                }
2318                return self.edit_circle(new_ast, sketch_id, owner_id, circle_ctor);
2319            }
2320
2321            // If owner is neither Line, Arc, nor Circle, allow editing the point directly
2322            // (fall through to the point editing logic below)
2323        }
2324
2325        // Modify the point AST.
2326        self.mutate_ast(new_ast, point_id, AstMutateCommand::EditPoint { at: new_at_ast })?;
2327        Ok(())
2328    }
2329
2330    fn edit_line(
2331        &mut self,
2332        new_ast: &mut ast::Node<ast::Program>,
2333        sketch: ObjectId,
2334        line: ObjectId,
2335        ctor: LineCtor,
2336    ) -> Result<(), KclError> {
2337        // Create updated KCL source from args.
2338        let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2339        let new_end_ast = to_ast_point2d(&ctor.end).map_err(|err| KclError::refactor(err.to_string()))?;
2340
2341        // Look up existing sketch.
2342        let sketch_id = sketch;
2343        let sketch_object = self
2344            .scene_graph
2345            .objects
2346            .get(sketch_id.0)
2347            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2348        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2349            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2350        };
2351        sketch
2352            .segments
2353            .iter()
2354            .find(|o| **o == line)
2355            .ok_or_else(|| KclError::refactor(format!("Line not found in sketch: line={line:?}, sketch={sketch:?}")))?;
2356        // Look up existing line.
2357        let line_id = line;
2358        let line_object = self
2359            .scene_graph
2360            .objects
2361            .get(line_id.0)
2362            .ok_or_else(|| KclError::refactor(format!("Line not found in scene graph: line={line:?}")))?;
2363        let ObjectKind::Segment { .. } = &line_object.kind else {
2364            let kind = line_object.kind.human_friendly_kind_with_article();
2365            return Err(KclError::refactor(format!(
2366                "This constraint only works on Segments, but you selected {kind}"
2367            )));
2368        };
2369
2370        // Modify the line AST.
2371        self.mutate_ast(
2372            new_ast,
2373            line_id,
2374            AstMutateCommand::EditLine {
2375                start: new_start_ast,
2376                end: new_end_ast,
2377                construction: ctor.construction,
2378            },
2379        )?;
2380        Ok(())
2381    }
2382
2383    fn edit_arc(
2384        &mut self,
2385        new_ast: &mut ast::Node<ast::Program>,
2386        sketch: ObjectId,
2387        arc: ObjectId,
2388        ctor: ArcCtor,
2389    ) -> Result<(), KclError> {
2390        // Create updated KCL source from args.
2391        let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2392        let new_end_ast = to_ast_point2d(&ctor.end).map_err(|err| KclError::refactor(err.to_string()))?;
2393        let new_center_ast = to_ast_point2d(&ctor.center).map_err(|err| KclError::refactor(err.to_string()))?;
2394
2395        // Look up existing sketch.
2396        let sketch_id = sketch;
2397        let sketch_object = self
2398            .scene_graph
2399            .objects
2400            .get(sketch_id.0)
2401            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2402        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2403            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2404        };
2405        sketch
2406            .segments
2407            .iter()
2408            .find(|o| **o == arc)
2409            .ok_or_else(|| KclError::refactor(format!("Arc not found in sketch: arc={arc:?}, sketch={sketch:?}")))?;
2410        // Look up existing arc.
2411        let arc_id = arc;
2412        let arc_object = self
2413            .scene_graph
2414            .objects
2415            .get(arc_id.0)
2416            .ok_or_else(|| KclError::refactor(format!("Arc not found in scene graph: arc={arc:?}")))?;
2417        let ObjectKind::Segment { .. } = &arc_object.kind else {
2418            return Err(KclError::refactor(format!("Object is not a segment: {arc_object:?}")));
2419        };
2420
2421        // Modify the arc AST.
2422        self.mutate_ast(
2423            new_ast,
2424            arc_id,
2425            AstMutateCommand::EditArc {
2426                start: new_start_ast,
2427                end: new_end_ast,
2428                center: new_center_ast,
2429                construction: ctor.construction,
2430            },
2431        )?;
2432        Ok(())
2433    }
2434
2435    fn edit_circle(
2436        &mut self,
2437        new_ast: &mut ast::Node<ast::Program>,
2438        sketch: ObjectId,
2439        circle: ObjectId,
2440        ctor: CircleCtor,
2441    ) -> Result<(), KclError> {
2442        // Create updated KCL source from args.
2443        let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2444        let new_center_ast = to_ast_point2d(&ctor.center).map_err(|err| KclError::refactor(err.to_string()))?;
2445
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.segments.iter().find(|o| **o == circle).ok_or_else(|| {
2457            KclError::refactor(format!(
2458                "Circle not found in sketch: circle={circle:?}, sketch={sketch:?}"
2459            ))
2460        })?;
2461        // Look up existing circle.
2462        let circle_id = circle;
2463        let circle_object = self
2464            .scene_graph
2465            .objects
2466            .get(circle_id.0)
2467            .ok_or_else(|| KclError::refactor(format!("Circle not found in scene graph: circle={circle:?}")))?;
2468        let ObjectKind::Segment { .. } = &circle_object.kind else {
2469            return Err(KclError::refactor(format!(
2470                "Object is not a segment: {circle_object:?}"
2471            )));
2472        };
2473
2474        // Modify the circle AST.
2475        self.mutate_ast(
2476            new_ast,
2477            circle_id,
2478            AstMutateCommand::EditCircle {
2479                start: new_start_ast,
2480                center: new_center_ast,
2481                construction: ctor.construction,
2482            },
2483        )?;
2484        Ok(())
2485    }
2486
2487    fn delete_segment(
2488        &mut self,
2489        new_ast: &mut ast::Node<ast::Program>,
2490        sketch: ObjectId,
2491        segment_id: ObjectId,
2492    ) -> Result<(), KclError> {
2493        // Look up existing sketch.
2494        let sketch_id = sketch;
2495        let sketch_object = self
2496            .scene_graph
2497            .objects
2498            .get(sketch_id.0)
2499            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2500        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2501            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2502        };
2503        sketch.segments.iter().find(|o| **o == segment_id).ok_or_else(|| {
2504            KclError::refactor(format!(
2505                "Segment not found in sketch: segment={segment_id:?}, sketch={sketch:?}"
2506            ))
2507        })?;
2508        // Look up existing segment.
2509        let segment_object =
2510            self.scene_graph.objects.get(segment_id.0).ok_or_else(|| {
2511                KclError::refactor(format!("Segment not found in scene graph: segment={segment_id:?}"))
2512            })?;
2513        let ObjectKind::Segment { .. } = &segment_object.kind else {
2514            return Err(KclError::refactor(format!(
2515                "Object is not a segment, it is {}",
2516                segment_object.kind.human_friendly_kind_with_article()
2517            )));
2518        };
2519
2520        // Modify the AST to remove the segment.
2521        self.mutate_ast(new_ast, segment_id, AstMutateCommand::DeleteNode)?;
2522        Ok(())
2523    }
2524
2525    fn delete_constraint(
2526        &mut self,
2527        new_ast: &mut ast::Node<ast::Program>,
2528        sketch: ObjectId,
2529        constraint_id: ObjectId,
2530    ) -> Result<(), KclError> {
2531        // Look up existing sketch.
2532        let sketch_id = sketch;
2533        let sketch_object = self
2534            .scene_graph
2535            .objects
2536            .get(sketch_id.0)
2537            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2538        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2539            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2540        };
2541        sketch
2542            .constraints
2543            .iter()
2544            .find(|o| **o == constraint_id)
2545            .ok_or_else(|| {
2546                KclError::refactor(format!(
2547                    "Constraint not found in sketch: constraint={constraint_id:?}, sketch={sketch:?}"
2548                ))
2549            })?;
2550        // Look up existing constraint.
2551        let constraint_object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
2552            KclError::refactor(format!(
2553                "Constraint not found in scene graph: constraint={constraint_id:?}"
2554            ))
2555        })?;
2556        let ObjectKind::Constraint { .. } = &constraint_object.kind else {
2557            return Err(KclError::refactor(format!(
2558                "Object is not a constraint, it is {}",
2559                constraint_object.kind.human_friendly_kind_with_article()
2560            )));
2561        };
2562
2563        // Modify the AST to remove the constraint.
2564        self.mutate_ast(new_ast, constraint_id, AstMutateCommand::DeleteNode)?;
2565        Ok(())
2566    }
2567
2568    fn edit_coincident_constraint(
2569        &mut self,
2570        new_ast: &mut ast::Node<ast::Program>,
2571        constraint_id: ObjectId,
2572        segments: Vec<ConstraintSegment>,
2573    ) -> Result<(), KclError> {
2574        if segments.len() < 2 {
2575            return Err(KclError::refactor(format!(
2576                "Coincident constraint must have at least 2 inputs, got {}",
2577                segments.len()
2578            )));
2579        }
2580
2581        let segment_asts = segments
2582            .iter()
2583            .map(|segment| self.coincident_segment_to_ast(segment, new_ast))
2584            .collect::<Result<Vec<_>, _>>()?;
2585
2586        let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
2587            elements: segment_asts,
2588            digest: None,
2589            non_code_meta: Default::default(),
2590        })));
2591
2592        self.mutate_ast(
2593            new_ast,
2594            constraint_id,
2595            AstMutateCommand::EditCallUnlabeled { arg: array_expr },
2596        )?;
2597        Ok(())
2598    }
2599
2600    fn edit_horizontal_points_constraint(
2601        &mut self,
2602        new_ast: &mut ast::Node<ast::Program>,
2603        constraint_id: ObjectId,
2604        points: Vec<ConstraintSegment>,
2605    ) -> Result<(), KclError> {
2606        self.edit_axis_points_constraint(new_ast, constraint_id, points, "Horizontal")
2607    }
2608
2609    fn edit_vertical_points_constraint(
2610        &mut self,
2611        new_ast: &mut ast::Node<ast::Program>,
2612        constraint_id: ObjectId,
2613        points: Vec<ConstraintSegment>,
2614    ) -> Result<(), KclError> {
2615        self.edit_axis_points_constraint(new_ast, constraint_id, points, "Vertical")
2616    }
2617
2618    fn edit_axis_points_constraint(
2619        &mut self,
2620        new_ast: &mut ast::Node<ast::Program>,
2621        constraint_id: ObjectId,
2622        points: Vec<ConstraintSegment>,
2623        constraint_name: &str,
2624    ) -> Result<(), KclError> {
2625        if points.len() < 2 {
2626            return Err(KclError::refactor(format!(
2627                "{constraint_name} points constraint must have at least 2 points, got {}",
2628                points.len()
2629            )));
2630        }
2631
2632        let point_asts = points
2633            .iter()
2634            .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
2635            .collect::<Result<Vec<_>, _>>()?;
2636
2637        let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
2638            elements: point_asts,
2639            digest: None,
2640            non_code_meta: Default::default(),
2641        })));
2642
2643        self.mutate_ast(
2644            new_ast,
2645            constraint_id,
2646            AstMutateCommand::EditCallUnlabeled { arg: array_expr },
2647        )?;
2648        Ok(())
2649    }
2650
2651    /// updates the equalLength constraint with the given lines
2652    fn edit_equal_length_constraint(
2653        &mut self,
2654        new_ast: &mut ast::Node<ast::Program>,
2655        constraint_id: ObjectId,
2656        lines: Vec<ObjectId>,
2657    ) -> Result<(), KclError> {
2658        if lines.len() < 2 {
2659            return Err(KclError::refactor(format!(
2660                "Lines equal length constraint must have at least 2 lines, got {}",
2661                lines.len()
2662            )));
2663        }
2664
2665        let line_asts = lines
2666            .iter()
2667            .map(|line_id| {
2668                let line_object = self
2669                    .scene_graph
2670                    .objects
2671                    .get(line_id.0)
2672                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
2673                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
2674                    let kind = line_object.kind.human_friendly_kind_with_article();
2675                    return Err(KclError::refactor(format!(
2676                        "This constraint only works on Segments, but you selected {kind}"
2677                    )));
2678                };
2679                let Segment::Line(_) = line_segment else {
2680                    let kind = line_segment.human_friendly_kind_with_article();
2681                    return Err(KclError::refactor(format!(
2682                        "Only lines can be made equal length, but you selected {kind}"
2683                    )));
2684                };
2685
2686                get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)
2687            })
2688            .collect::<Result<Vec<_>, _>>()?;
2689
2690        let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
2691            elements: line_asts,
2692            digest: None,
2693            non_code_meta: Default::default(),
2694        })));
2695
2696        self.mutate_ast(
2697            new_ast,
2698            constraint_id,
2699            AstMutateCommand::EditCallUnlabeled { arg: array_expr },
2700        )?;
2701        Ok(())
2702    }
2703
2704    /// Updates the parallel constraint with the given lines.
2705    fn edit_parallel_constraint(
2706        &mut self,
2707        new_ast: &mut ast::Node<ast::Program>,
2708        constraint_id: ObjectId,
2709        lines: Vec<ObjectId>,
2710    ) -> Result<(), KclError> {
2711        if lines.len() < 2 {
2712            return Err(KclError::refactor(format!(
2713                "Parallel constraint must have at least 2 lines, got {}",
2714                lines.len()
2715            )));
2716        }
2717
2718        let line_asts = lines
2719            .iter()
2720            .map(|line_id| {
2721                let line_object = self
2722                    .scene_graph
2723                    .objects
2724                    .get(line_id.0)
2725                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
2726                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
2727                    let kind = line_object.kind.human_friendly_kind_with_article();
2728                    return Err(KclError::refactor(format!(
2729                        "This constraint only works on Segments, but you selected {kind}"
2730                    )));
2731                };
2732                let Segment::Line(_) = line_segment else {
2733                    let kind = line_segment.human_friendly_kind_with_article();
2734                    return Err(KclError::refactor(format!(
2735                        "Only lines can be made parallel, but you selected {kind}"
2736                    )));
2737                };
2738
2739                get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)
2740            })
2741            .collect::<Result<Vec<_>, _>>()?;
2742
2743        let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
2744            elements: line_asts,
2745            digest: None,
2746            non_code_meta: Default::default(),
2747        })));
2748
2749        self.mutate_ast(
2750            new_ast,
2751            constraint_id,
2752            AstMutateCommand::EditCallUnlabeled { arg: array_expr },
2753        )?;
2754        Ok(())
2755    }
2756
2757    /// Updates the equalRadius constraint with the given segments.
2758    fn edit_equal_radius_constraint(
2759        &mut self,
2760        new_ast: &mut ast::Node<ast::Program>,
2761        constraint_id: ObjectId,
2762        input: Vec<ObjectId>,
2763    ) -> Result<(), KclError> {
2764        if input.len() < 2 {
2765            return Err(KclError::refactor(format!(
2766                "equalRadius constraint must have at least 2 segments, got {}",
2767                input.len()
2768            )));
2769        }
2770
2771        let input_asts = input
2772            .iter()
2773            .map(|segment_id| self.equal_radius_segment_id_to_ast_reference(*segment_id, new_ast))
2774            .collect::<Result<Vec<_>, _>>()?;
2775
2776        let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
2777            elements: input_asts,
2778            digest: None,
2779            non_code_meta: Default::default(),
2780        })));
2781
2782        self.mutate_ast(
2783            new_ast,
2784            constraint_id,
2785            AstMutateCommand::EditCallUnlabeled { arg: array_expr },
2786        )?;
2787        Ok(())
2788    }
2789
2790    async fn execute_after_edit(
2791        &mut self,
2792        ctx: &ExecutorContext,
2793        sketch: ObjectId,
2794        sketch_block_ref: AstNodeRef,
2795        segment_ids_edited: AhashIndexSet<ObjectId>,
2796        edit_kind: EditDeleteKind,
2797        new_ast: &mut ast::Node<ast::Program>,
2798    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2799        // Convert to string source to create real source ranges.
2800        let new_source = source_from_ast(new_ast);
2801        // Parse the new KCL source.
2802        let (new_program, errors) = Program::parse(&new_source)
2803            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2804        if !errors.is_empty() {
2805            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2806                "Error parsing KCL source after editing: {errors:?}"
2807            ))));
2808        }
2809        let Some(new_program) = new_program else {
2810            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
2811                "No AST produced after editing".to_string(),
2812            )));
2813        };
2814
2815        // TODO: sketch-api: make sure to only set this if there are no errors.
2816        self.program = new_program.clone();
2817
2818        // Truncate after the sketch block for mock execution.
2819        let is_delete = edit_kind.is_delete();
2820        let truncated_program = {
2821            let mut truncated_program = new_program;
2822            only_sketch_block(
2823                &mut truncated_program.ast,
2824                &sketch_block_ref,
2825                edit_kind.to_change_kind(),
2826            )
2827            .map_err(KclErrorWithOutputs::no_outputs)?;
2828            truncated_program
2829        };
2830
2831        #[cfg(not(feature = "artifact-graph"))]
2832        drop(segment_ids_edited);
2833
2834        // Execute.
2835        let mock_config = MockConfig {
2836            sketch_block_id: Some(sketch),
2837            freedom_analysis: is_delete,
2838            #[cfg(feature = "artifact-graph")]
2839            segment_ids_edited: segment_ids_edited.clone(),
2840            ..Default::default()
2841        };
2842        let outcome = ctx.run_mock(&truncated_program, &mock_config).await?;
2843
2844        // Uses freedom_analysis: is_delete
2845        let outcome = self.update_state_after_exec(outcome, is_delete);
2846
2847        #[cfg(feature = "artifact-graph")]
2848        let new_source = {
2849            // Feed back sketch var solutions into the source.
2850            //
2851            // The interpreter is returning all var solutions from the sketch
2852            // block we're editing.
2853            let mut new_ast = self.program.ast.clone();
2854            for (var_range, value) in &outcome.var_solutions {
2855                let rounded = value.round(3);
2856                let source_ref = SourceRef::Simple {
2857                    range: *var_range,
2858                    node_path: None,
2859                };
2860                mutate_ast_node_by_source_ref(
2861                    &mut new_ast,
2862                    &source_ref,
2863                    AstMutateCommand::EditVarInitialValue { value: rounded },
2864                )
2865                .map_err(|err| KclErrorWithOutputs::from_error_outcome(err, outcome.clone()))?;
2866            }
2867            source_from_ast(&new_ast)
2868        };
2869
2870        let src_delta = SourceDelta { text: new_source };
2871        let scene_graph_delta = SceneGraphDelta {
2872            new_graph: self.scene_graph.clone(),
2873            invalidates_ids: is_delete,
2874            new_objects: Vec::new(),
2875            exec_outcome: outcome,
2876        };
2877        Ok((src_delta, scene_graph_delta))
2878    }
2879
2880    async fn execute_after_delete_sketch(
2881        &mut self,
2882        ctx: &ExecutorContext,
2883        new_ast: &mut ast::Node<ast::Program>,
2884    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2885        // Convert to string source to create real source ranges.
2886        let new_source = source_from_ast(new_ast);
2887        // Parse the new KCL source.
2888        let (new_program, errors) = Program::parse(&new_source)
2889            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2890        if !errors.is_empty() {
2891            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2892                "Error parsing KCL source after editing: {errors:?}"
2893            ))));
2894        }
2895        let Some(new_program) = new_program else {
2896            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
2897                "No AST produced after editing".to_string(),
2898            )));
2899        };
2900
2901        // Make sure to only set this if there are no errors.
2902        self.program = new_program.clone();
2903
2904        // We deleted the entire sketch block. It doesn't make sense to truncate
2905        // and execute only the sketch block. We execute the whole program with
2906        // a real engine.
2907
2908        // Execute.
2909        let outcome = ctx.run_with_caching(new_program).await?;
2910        let freedom_analysis_ran = true;
2911
2912        let outcome = self.update_state_after_exec(outcome, freedom_analysis_ran);
2913
2914        let src_delta = SourceDelta { text: new_source };
2915        let scene_graph_delta = SceneGraphDelta {
2916            new_graph: self.scene_graph.clone(),
2917            invalidates_ids: true,
2918            new_objects: Vec::new(),
2919            exec_outcome: outcome,
2920        };
2921        Ok((src_delta, scene_graph_delta))
2922    }
2923
2924    /// Map a point object id into an AST reference expression for use in
2925    /// constraints. If the point is owned by a segment (line or arc), we
2926    /// reference the appropriate property on that segment (e.g. `line1.start`,
2927    /// `arc1.center`). Otherwise we reference the point directly.
2928    fn point_id_to_ast_reference(
2929        &self,
2930        point_id: ObjectId,
2931        new_ast: &mut ast::Node<ast::Program>,
2932    ) -> Result<ast::Expr, KclError> {
2933        let point_object = self
2934            .scene_graph
2935            .objects
2936            .get(point_id.0)
2937            .ok_or_else(|| KclError::refactor(format!("Point not found: {point_id:?}")))?;
2938        let ObjectKind::Segment { segment: point_segment } = &point_object.kind else {
2939            return Err(KclError::refactor(format!("Object is not a segment: {point_object:?}")));
2940        };
2941        let Segment::Point(point) = point_segment else {
2942            return Err(KclError::refactor(format!(
2943                "Only points are currently supported: {point_object:?}"
2944            )));
2945        };
2946
2947        if let Some(owner_id) = point.owner {
2948            let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
2949                KclError::refactor(format!(
2950                    "Owner of point not found in scene graph: point={point_id:?}, owner={owner_id:?}"
2951                ))
2952            })?;
2953            let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind else {
2954                return Err(KclError::refactor(format!(
2955                    "Owner of point is not a segment, but found {}",
2956                    owner_object.kind.human_friendly_kind_with_article()
2957                )));
2958            };
2959
2960            match owner_segment {
2961                Segment::Line(line) => {
2962                    let property = if line.start == point_id {
2963                        LINE_PROPERTY_START
2964                    } else if line.end == point_id {
2965                        LINE_PROPERTY_END
2966                    } else {
2967                        return Err(KclError::refactor(format!(
2968                            "Internal: Point is not part of owner's line segment: point={point_id:?}, line={owner_id:?}"
2969                        )));
2970                    };
2971                    get_or_insert_ast_reference(new_ast, &owner_object.source, LINE_VARIABLE, Some(property))
2972                }
2973                Segment::Arc(arc) => {
2974                    let property = if arc.start == point_id {
2975                        ARC_PROPERTY_START
2976                    } else if arc.end == point_id {
2977                        ARC_PROPERTY_END
2978                    } else if arc.center == point_id {
2979                        ARC_PROPERTY_CENTER
2980                    } else {
2981                        return Err(KclError::refactor(format!(
2982                            "Internal: Point is not part of owner's arc segment: point={point_id:?}, arc={owner_id:?}"
2983                        )));
2984                    };
2985                    get_or_insert_ast_reference(new_ast, &owner_object.source, ARC_VARIABLE, Some(property))
2986                }
2987                Segment::Circle(circle) => {
2988                    let property = if circle.start == point_id {
2989                        CIRCLE_PROPERTY_START
2990                    } else if circle.center == point_id {
2991                        CIRCLE_PROPERTY_CENTER
2992                    } else {
2993                        return Err(KclError::refactor(format!(
2994                            "Internal: Point is not part of owner's circle segment: point={point_id:?}, circle={owner_id:?}"
2995                        )));
2996                    };
2997                    get_or_insert_ast_reference(new_ast, &owner_object.source, CIRCLE_VARIABLE, Some(property))
2998                }
2999                _ => Err(KclError::refactor(format!(
3000                    "Internal: Owner of point is not a supported segment type for constraints: {owner_segment:?}"
3001                ))),
3002            }
3003        } else {
3004            // Standalone point.
3005            get_or_insert_ast_reference(new_ast, &point_object.source, "point", None)
3006        }
3007    }
3008
3009    fn coincident_segment_to_ast(
3010        &self,
3011        segment: &ConstraintSegment,
3012        new_ast: &mut ast::Node<ast::Program>,
3013    ) -> Result<ast::Expr, KclError> {
3014        match segment {
3015            ConstraintSegment::Origin(_) => Ok(ast_name_expr("ORIGIN".to_owned())),
3016            ConstraintSegment::Segment(segment_id) => {
3017                let segment_object = self
3018                    .scene_graph
3019                    .objects
3020                    .get(segment_id.0)
3021                    .ok_or_else(|| KclError::refactor(format!("Object not found: {segment_id:?}")))?;
3022                let ObjectKind::Segment { segment } = &segment_object.kind else {
3023                    return Err(KclError::refactor(format!(
3024                        "Object is not a segment, it is {}",
3025                        segment_object.kind.human_friendly_kind_with_article()
3026                    )));
3027                };
3028
3029                match segment {
3030                    Segment::Point(_) => self.point_id_to_ast_reference(*segment_id, new_ast),
3031                    Segment::Line(_) => {
3032                        get_or_insert_ast_reference(new_ast, &segment_object.source, LINE_VARIABLE, None)
3033                    }
3034                    Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, ARC_VARIABLE, None),
3035                    Segment::Circle(_) => {
3036                        get_or_insert_ast_reference(new_ast, &segment_object.source, CIRCLE_VARIABLE, None)
3037                    }
3038                }
3039            }
3040        }
3041    }
3042
3043    fn axis_constraint_segment_to_ast(
3044        &self,
3045        segment: &ConstraintSegment,
3046        new_ast: &mut ast::Node<ast::Program>,
3047    ) -> Result<ast::Expr, KclError> {
3048        match segment {
3049            ConstraintSegment::Origin(_) => Ok(ast_name_expr("ORIGIN".to_owned())),
3050            ConstraintSegment::Segment(point_id) => self.point_id_to_ast_reference(*point_id, new_ast),
3051        }
3052    }
3053
3054    async fn add_coincident(
3055        &mut self,
3056        sketch: ObjectId,
3057        coincident: Coincident,
3058        new_ast: &mut ast::Node<ast::Program>,
3059    ) -> Result<AstNodeRef, KclError> {
3060        let sketch_id = sketch;
3061        let segment_asts = coincident
3062            .segments
3063            .iter()
3064            .map(|segment| self.coincident_segment_to_ast(segment, new_ast))
3065            .collect::<Result<Vec<_>, _>>()?;
3066        if segment_asts.len() < 2 {
3067            return Err(KclError::refactor(format!(
3068                "Coincident constraint must have at least 2 inputs, got {}",
3069                segment_asts.len()
3070            )));
3071        }
3072
3073        // Create the coincident() call using shared helper.
3074        let coincident_ast = create_coincident_ast(segment_asts);
3075
3076        // Add the line to the AST of the sketch block.
3077        let (sketch_block_ref, _) = self.mutate_ast(
3078            new_ast,
3079            sketch_id,
3080            AstMutateCommand::AddSketchBlockExprStmt { expr: coincident_ast },
3081        )?;
3082        Ok(sketch_block_ref)
3083    }
3084
3085    async fn add_distance(
3086        &mut self,
3087        sketch: ObjectId,
3088        distance: Distance,
3089        new_ast: &mut ast::Node<ast::Program>,
3090    ) -> Result<AstNodeRef, KclError> {
3091        let sketch_id = sketch;
3092        let [pt0_ast, pt1_ast] = match distance.points.as_slice() {
3093            [pt0, pt1] => [
3094                self.coincident_segment_to_ast(pt0, new_ast)?,
3095                self.coincident_segment_to_ast(pt1, new_ast)?,
3096            ],
3097            _ => {
3098                return Err(KclError::refactor(format!(
3099                    "Distance constraint must have exactly 2 points, got {}",
3100                    distance.points.len()
3101                )));
3102            }
3103        };
3104
3105        let arguments = match &distance.label_position {
3106            Some(label_position) => vec![ast::LabeledArg {
3107                label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
3108                arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
3109            }],
3110            None => Default::default(),
3111        };
3112
3113        // Create the distance() call.
3114        let distance_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3115            callee: ast::Node::no_src(ast_sketch2_name(DISTANCE_FN)),
3116            unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
3117                ast::ArrayExpression {
3118                    elements: vec![pt0_ast, pt1_ast],
3119                    digest: None,
3120                    non_code_meta: Default::default(),
3121                },
3122            )))),
3123            arguments,
3124            digest: None,
3125            non_code_meta: Default::default(),
3126        })));
3127        let distance_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
3128            left: distance_call_ast,
3129            operator: ast::BinaryOperator::Eq,
3130            right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
3131                value: ast::LiteralValue::Number {
3132                    value: distance.distance.value,
3133                    suffix: distance.distance.units,
3134                },
3135                raw: format_number_literal(distance.distance.value, distance.distance.units, None).map_err(|_| {
3136                    KclError::refactor(format!(
3137                        "Could not format numeric suffix: {:?}",
3138                        distance.distance.units
3139                    ))
3140                })?,
3141                digest: None,
3142            }))),
3143            digest: None,
3144        })));
3145
3146        // Add the line to the AST of the sketch block.
3147        let (sketch_block_ref, _) = self.mutate_ast(
3148            new_ast,
3149            sketch_id,
3150            AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
3151        )?;
3152        Ok(sketch_block_ref)
3153    }
3154
3155    async fn add_angle(
3156        &mut self,
3157        sketch: ObjectId,
3158        angle: Angle,
3159        new_ast: &mut ast::Node<ast::Program>,
3160    ) -> Result<AstNodeRef, KclError> {
3161        let &[l0_id, l1_id] = angle.lines.as_slice() else {
3162            return Err(KclError::refactor(format!(
3163                "Angle constraint must have exactly 2 lines, got {}",
3164                angle.lines.len()
3165            )));
3166        };
3167        let sketch_id = sketch;
3168
3169        // Map the runtime objects back to variable names.
3170        let line0_object = self
3171            .scene_graph
3172            .objects
3173            .get(l0_id.0)
3174            .ok_or_else(|| KclError::refactor(format!("Line not found: {l0_id:?}")))?;
3175        let ObjectKind::Segment { segment: line0_segment } = &line0_object.kind else {
3176            return Err(KclError::refactor(format!("Object is not a segment: {line0_object:?}")));
3177        };
3178        let Segment::Line(_) = line0_segment else {
3179            return Err(KclError::refactor(format!(
3180                "Only lines can be constrained to meet at an angle: {line0_object:?}",
3181            )));
3182        };
3183        let l0_ast = get_or_insert_ast_reference(new_ast, &line0_object.source.clone(), LINE_VARIABLE, None)?;
3184
3185        let line1_object = self
3186            .scene_graph
3187            .objects
3188            .get(l1_id.0)
3189            .ok_or_else(|| KclError::refactor(format!("Line not found: {l1_id:?}")))?;
3190        let ObjectKind::Segment { segment: line1_segment } = &line1_object.kind else {
3191            return Err(KclError::refactor(format!("Object is not a segment: {line1_object:?}")));
3192        };
3193        let Segment::Line(_) = line1_segment else {
3194            return Err(KclError::refactor(format!(
3195                "Only lines can be constrained to meet at an angle: {line1_object:?}",
3196            )));
3197        };
3198        let l1_ast = get_or_insert_ast_reference(new_ast, &line1_object.source.clone(), LINE_VARIABLE, None)?;
3199
3200        // Create the angle() call.
3201        let angle_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3202            callee: ast::Node::no_src(ast_sketch2_name(ANGLE_FN)),
3203            unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
3204                ast::ArrayExpression {
3205                    elements: vec![l0_ast, l1_ast],
3206                    digest: None,
3207                    non_code_meta: Default::default(),
3208                },
3209            )))),
3210            arguments: Default::default(),
3211            digest: None,
3212            non_code_meta: Default::default(),
3213        })));
3214        let angle_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
3215            left: angle_call_ast,
3216            operator: ast::BinaryOperator::Eq,
3217            right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
3218                value: ast::LiteralValue::Number {
3219                    value: angle.angle.value,
3220                    suffix: angle.angle.units,
3221                },
3222                raw: format_number_literal(angle.angle.value, angle.angle.units, None).map_err(|_| {
3223                    KclError::refactor(format!("Could not format numeric suffix: {:?}", angle.angle.units))
3224                })?,
3225                digest: None,
3226            }))),
3227            digest: None,
3228        })));
3229
3230        // Add the line to the AST of the sketch block.
3231        let (sketch_block_ref, _) = self.mutate_ast(
3232            new_ast,
3233            sketch_id,
3234            AstMutateCommand::AddSketchBlockExprStmt { expr: angle_ast },
3235        )?;
3236        Ok(sketch_block_ref)
3237    }
3238
3239    async fn add_tangent(
3240        &mut self,
3241        sketch: ObjectId,
3242        tangent: Tangent,
3243        new_ast: &mut ast::Node<ast::Program>,
3244    ) -> Result<AstNodeRef, KclError> {
3245        let &[seg0_id, seg1_id] = tangent.input.as_slice() else {
3246            return Err(KclError::refactor(format!(
3247                "Tangent constraint must have exactly 2 segments, got {}",
3248                tangent.input.len()
3249            )));
3250        };
3251        let sketch_id = sketch;
3252
3253        let seg0_object = self
3254            .scene_graph
3255            .objects
3256            .get(seg0_id.0)
3257            .ok_or_else(|| KclError::refactor(format!("Segment not found: {seg0_id:?}")))?;
3258        let ObjectKind::Segment { segment: seg0_segment } = &seg0_object.kind else {
3259            return Err(KclError::refactor(format!("Object is not a segment: {seg0_object:?}")));
3260        };
3261        let seg0_ast = match seg0_segment {
3262            Segment::Line(_) => get_or_insert_ast_reference(new_ast, &seg0_object.source, LINE_VARIABLE, None)?,
3263            Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &seg0_object.source, ARC_VARIABLE, None)?,
3264            Segment::Circle(_) => get_or_insert_ast_reference(new_ast, &seg0_object.source, CIRCLE_VARIABLE, None)?,
3265            _ => {
3266                return Err(KclError::refactor(format!(
3267                    "Tangent supports only line/arc/circle segments, got: {seg0_segment:?}"
3268                )));
3269            }
3270        };
3271
3272        let seg1_object = self
3273            .scene_graph
3274            .objects
3275            .get(seg1_id.0)
3276            .ok_or_else(|| KclError::refactor(format!("Segment not found: {seg1_id:?}")))?;
3277        let ObjectKind::Segment { segment: seg1_segment } = &seg1_object.kind else {
3278            return Err(KclError::refactor(format!("Object is not a segment: {seg1_object:?}")));
3279        };
3280        let seg1_ast = match seg1_segment {
3281            Segment::Line(_) => get_or_insert_ast_reference(new_ast, &seg1_object.source, LINE_VARIABLE, None)?,
3282            Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &seg1_object.source, ARC_VARIABLE, None)?,
3283            Segment::Circle(_) => get_or_insert_ast_reference(new_ast, &seg1_object.source, CIRCLE_VARIABLE, None)?,
3284            _ => {
3285                return Err(KclError::refactor(format!(
3286                    "Tangent supports only line/arc/circle segments, got: {seg1_segment:?}"
3287                )));
3288            }
3289        };
3290
3291        let tangent_ast = create_tangent_ast(seg0_ast, seg1_ast);
3292        let (sketch_block_ref, _) = self.mutate_ast(
3293            new_ast,
3294            sketch_id,
3295            AstMutateCommand::AddSketchBlockExprStmt { expr: tangent_ast },
3296        )?;
3297        Ok(sketch_block_ref)
3298    }
3299
3300    async fn add_symmetric(
3301        &mut self,
3302        sketch: ObjectId,
3303        symmetric: Symmetric,
3304        new_ast: &mut ast::Node<ast::Program>,
3305    ) -> Result<AstNodeRef, KclError> {
3306        let &[input0_id, input1_id] = symmetric.input.as_slice() else {
3307            return Err(KclError::refactor(format!(
3308                "Symmetric constraint must have exactly 2 inputs, got {}",
3309                symmetric.input.len()
3310            )));
3311        };
3312        let sketch_id = sketch;
3313
3314        let input0_ast = self.symmetric_input_id_to_ast_reference(input0_id, new_ast)?;
3315        let input1_ast = self.symmetric_input_id_to_ast_reference(input1_id, new_ast)?;
3316        let axis_ast = self.symmetric_axis_id_to_ast_reference(symmetric.axis, new_ast)?;
3317
3318        let symmetric_ast = create_symmetric_ast(vec![input0_ast, input1_ast], axis_ast);
3319        let (sketch_block_ref, _) = self.mutate_ast(
3320            new_ast,
3321            sketch_id,
3322            AstMutateCommand::AddSketchBlockExprStmt { expr: symmetric_ast },
3323        )?;
3324        Ok(sketch_block_ref)
3325    }
3326
3327    async fn add_midpoint(
3328        &mut self,
3329        sketch: ObjectId,
3330        midpoint: Midpoint,
3331        new_ast: &mut ast::Node<ast::Program>,
3332    ) -> Result<AstNodeRef, KclError> {
3333        let sketch_id = sketch;
3334        let point_ast = self.point_id_to_ast_reference(midpoint.point, new_ast)?;
3335
3336        let segment_object = self
3337            .scene_graph
3338            .objects
3339            .get(midpoint.segment.0)
3340            .ok_or_else(|| KclError::refactor(format!("Segment not found: {:?}", midpoint.segment)))?;
3341        let ObjectKind::Segment {
3342            segment: midpoint_segment,
3343        } = &segment_object.kind
3344        else {
3345            return Err(KclError::refactor(format!(
3346                "Object must be a segment, but it was {}",
3347                segment_object.kind.human_friendly_kind_with_article()
3348            )));
3349        };
3350        let segment_ast = match midpoint_segment {
3351            Segment::Line(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, "line", None)?,
3352            Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, "arc", None)?,
3353            _ => {
3354                return Err(KclError::refactor(format!(
3355                    "Midpoint target must be a line or arc segment but it was {}",
3356                    midpoint_segment.human_friendly_kind_with_article()
3357                )));
3358            }
3359        };
3360
3361        let midpoint_ast = create_midpoint_ast(segment_ast, point_ast);
3362        let (sketch_block_ref, _) = self.mutate_ast(
3363            new_ast,
3364            sketch_id,
3365            AstMutateCommand::AddSketchBlockExprStmt { expr: midpoint_ast },
3366        )?;
3367        Ok(sketch_block_ref)
3368    }
3369
3370    async fn add_equal_radius(
3371        &mut self,
3372        sketch: ObjectId,
3373        equal_radius: EqualRadius,
3374        new_ast: &mut ast::Node<ast::Program>,
3375    ) -> Result<AstNodeRef, KclError> {
3376        if equal_radius.input.len() < 2 {
3377            return Err(KclError::refactor(format!(
3378                "equalRadius constraint must have at least 2 segments, got {}",
3379                equal_radius.input.len()
3380            )));
3381        }
3382
3383        let sketch_id = sketch;
3384        let input_asts = equal_radius
3385            .input
3386            .iter()
3387            .map(|segment_id| self.equal_radius_segment_id_to_ast_reference(*segment_id, new_ast))
3388            .collect::<Result<Vec<_>, _>>()?;
3389
3390        let equal_radius_ast = create_equal_radius_ast(input_asts);
3391        let (sketch_block_ref, _) = self.mutate_ast(
3392            new_ast,
3393            sketch_id,
3394            AstMutateCommand::AddSketchBlockExprStmt { expr: equal_radius_ast },
3395        )?;
3396        Ok(sketch_block_ref)
3397    }
3398
3399    async fn add_radius(
3400        &mut self,
3401        sketch: ObjectId,
3402        radius: Radius,
3403        new_ast: &mut ast::Node<ast::Program>,
3404    ) -> Result<AstNodeRef, KclError> {
3405        let params = ArcSizeConstraintParams {
3406            points: vec![radius.arc],
3407            function_name: RADIUS_FN,
3408            value: radius.radius.value,
3409            units: radius.radius.units,
3410            constraint_type_name: "Radius",
3411        };
3412        self.add_arc_size_constraint(sketch, params, new_ast).await
3413    }
3414
3415    async fn add_diameter(
3416        &mut self,
3417        sketch: ObjectId,
3418        diameter: Diameter,
3419        new_ast: &mut ast::Node<ast::Program>,
3420    ) -> Result<AstNodeRef, KclError> {
3421        let params = ArcSizeConstraintParams {
3422            points: vec![diameter.arc],
3423            function_name: DIAMETER_FN,
3424            value: diameter.diameter.value,
3425            units: diameter.diameter.units,
3426            constraint_type_name: "Diameter",
3427        };
3428        self.add_arc_size_constraint(sketch, params, new_ast).await
3429    }
3430
3431    async fn add_fixed_constraints(
3432        &mut self,
3433        sketch: ObjectId,
3434        points: Vec<FixedPoint>,
3435        new_ast: &mut ast::Node<ast::Program>,
3436    ) -> Result<AstNodeRef, KclError> {
3437        let mut sketch_block_ref = None;
3438
3439        for fixed_point in points {
3440            let point_ast = self.point_id_to_ast_reference(fixed_point.point, new_ast)?;
3441            let fixed_ast = create_fixed_point_constraint_ast(point_ast, fixed_point.position)
3442                .map_err(|err| KclError::refactor(err.to_string()))?;
3443
3444            let (sketch_ref, _) = self.mutate_ast(
3445                new_ast,
3446                sketch,
3447                AstMutateCommand::AddSketchBlockExprStmt { expr: fixed_ast },
3448            )?;
3449            sketch_block_ref = Some(sketch_ref);
3450        }
3451
3452        sketch_block_ref.ok_or_else(|| KclError::refactor("Fixed constraint requires at least one point".to_owned()))
3453    }
3454
3455    async fn add_arc_size_constraint(
3456        &mut self,
3457        sketch: ObjectId,
3458        params: ArcSizeConstraintParams,
3459        new_ast: &mut ast::Node<ast::Program>,
3460    ) -> Result<AstNodeRef, KclError> {
3461        let sketch_id = sketch;
3462
3463        // Constraint must have exactly 1 argument (arc segment)
3464        if params.points.len() != 1 {
3465            return Err(KclError::refactor(format!(
3466                "{} constraint must have exactly 1 argument (an arc segment), got {}",
3467                params.constraint_type_name,
3468                params.points.len()
3469            )));
3470        }
3471
3472        let arc_id = params.points[0];
3473        let arc_object = self
3474            .scene_graph
3475            .objects
3476            .get(arc_id.0)
3477            .ok_or_else(|| KclError::refactor(format!("Arc segment not found: {arc_id:?}")))?;
3478        let ObjectKind::Segment { segment: arc_segment } = &arc_object.kind else {
3479            return Err(KclError::refactor(format!("Object is not a segment: {arc_object:?}")));
3480        };
3481        let ref_type = match arc_segment {
3482            Segment::Arc(_) => ARC_VARIABLE,
3483            Segment::Circle(_) => CIRCLE_VARIABLE,
3484            _ => {
3485                return Err(KclError::refactor(format!(
3486                    "{} constraint argument must be an arc or circle segment, got: {arc_segment:?}",
3487                    params.constraint_type_name
3488                )));
3489            }
3490        };
3491        // Reference the arc/circle segment directly
3492        let arc_ast = get_or_insert_ast_reference(new_ast, &arc_object.source, ref_type, None)?;
3493
3494        // Create the function call.
3495        let call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3496            callee: ast::Node::no_src(ast_sketch2_name(params.function_name)),
3497            unlabeled: Some(arc_ast),
3498            arguments: Default::default(),
3499            digest: None,
3500            non_code_meta: Default::default(),
3501        })));
3502        let constraint_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
3503            left: call_ast,
3504            operator: ast::BinaryOperator::Eq,
3505            right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
3506                value: ast::LiteralValue::Number {
3507                    value: params.value,
3508                    suffix: params.units,
3509                },
3510                raw: format_number_literal(params.value, params.units, None)
3511                    .map_err(|_| KclError::refactor(format!("Could not format numeric suffix: {:?}", params.units)))?,
3512                digest: None,
3513            }))),
3514            digest: None,
3515        })));
3516
3517        // Add the line to the AST of the sketch block.
3518        let (sketch_block_ref, _) = self.mutate_ast(
3519            new_ast,
3520            sketch_id,
3521            AstMutateCommand::AddSketchBlockExprStmt { expr: constraint_ast },
3522        )?;
3523        Ok(sketch_block_ref)
3524    }
3525
3526    async fn add_horizontal_distance(
3527        &mut self,
3528        sketch: ObjectId,
3529        distance: Distance,
3530        new_ast: &mut ast::Node<ast::Program>,
3531    ) -> Result<AstNodeRef, KclError> {
3532        let sketch_id = sketch;
3533        let [pt0_ast, pt1_ast] = match distance.points.as_slice() {
3534            [pt0, pt1] => [
3535                self.coincident_segment_to_ast(pt0, new_ast)?,
3536                self.coincident_segment_to_ast(pt1, new_ast)?,
3537            ],
3538            _ => {
3539                return Err(KclError::refactor(format!(
3540                    "Horizontal distance constraint must have exactly 2 points, got {}",
3541                    distance.points.len()
3542                )));
3543            }
3544        };
3545
3546        let arguments = match &distance.label_position {
3547            Some(label_position) => vec![ast::LabeledArg {
3548                label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
3549                arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
3550            }],
3551            None => Default::default(),
3552        };
3553
3554        // Create the horizontalDistance() call.
3555        let distance_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3556            callee: ast::Node::no_src(ast_sketch2_name(HORIZONTAL_DISTANCE_FN)),
3557            unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
3558                ast::ArrayExpression {
3559                    elements: vec![pt0_ast, pt1_ast],
3560                    digest: None,
3561                    non_code_meta: Default::default(),
3562                },
3563            )))),
3564            arguments,
3565            digest: None,
3566            non_code_meta: Default::default(),
3567        })));
3568        let distance_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
3569            left: distance_call_ast,
3570            operator: ast::BinaryOperator::Eq,
3571            right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
3572                value: ast::LiteralValue::Number {
3573                    value: distance.distance.value,
3574                    suffix: distance.distance.units,
3575                },
3576                raw: format_number_literal(distance.distance.value, distance.distance.units, None).map_err(|_| {
3577                    KclError::refactor(format!(
3578                        "Could not format numeric suffix: {:?}",
3579                        distance.distance.units
3580                    ))
3581                })?,
3582                digest: None,
3583            }))),
3584            digest: None,
3585        })));
3586
3587        // Add the line to the AST of the sketch block.
3588        let (sketch_block_ref, _) = self.mutate_ast(
3589            new_ast,
3590            sketch_id,
3591            AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
3592        )?;
3593        Ok(sketch_block_ref)
3594    }
3595
3596    async fn add_vertical_distance(
3597        &mut self,
3598        sketch: ObjectId,
3599        distance: Distance,
3600        new_ast: &mut ast::Node<ast::Program>,
3601    ) -> Result<AstNodeRef, KclError> {
3602        let sketch_id = sketch;
3603        let [pt0_ast, pt1_ast] = match distance.points.as_slice() {
3604            [pt0, pt1] => [
3605                self.coincident_segment_to_ast(pt0, new_ast)?,
3606                self.coincident_segment_to_ast(pt1, new_ast)?,
3607            ],
3608            _ => {
3609                return Err(KclError::refactor(format!(
3610                    "Vertical distance constraint must have exactly 2 points, got {}",
3611                    distance.points.len()
3612                )));
3613            }
3614        };
3615
3616        let arguments = match &distance.label_position {
3617            Some(label_position) => vec![ast::LabeledArg {
3618                label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
3619                arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
3620            }],
3621            None => Default::default(),
3622        };
3623
3624        // Create the verticalDistance() call.
3625        let distance_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3626            callee: ast::Node::no_src(ast_sketch2_name(VERTICAL_DISTANCE_FN)),
3627            unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
3628                ast::ArrayExpression {
3629                    elements: vec![pt0_ast, pt1_ast],
3630                    digest: None,
3631                    non_code_meta: Default::default(),
3632                },
3633            )))),
3634            arguments,
3635            digest: None,
3636            non_code_meta: Default::default(),
3637        })));
3638        let distance_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
3639            left: distance_call_ast,
3640            operator: ast::BinaryOperator::Eq,
3641            right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
3642                value: ast::LiteralValue::Number {
3643                    value: distance.distance.value,
3644                    suffix: distance.distance.units,
3645                },
3646                raw: format_number_literal(distance.distance.value, distance.distance.units, None).map_err(|_| {
3647                    KclError::refactor(format!(
3648                        "Could not format numeric suffix: {:?}",
3649                        distance.distance.units
3650                    ))
3651                })?,
3652                digest: None,
3653            }))),
3654            digest: None,
3655        })));
3656
3657        // Add the line to the AST of the sketch block.
3658        let (sketch_block_ref, _) = self.mutate_ast(
3659            new_ast,
3660            sketch_id,
3661            AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
3662        )?;
3663        Ok(sketch_block_ref)
3664    }
3665
3666    async fn add_horizontal(
3667        &mut self,
3668        sketch: ObjectId,
3669        horizontal: Horizontal,
3670        new_ast: &mut ast::Node<ast::Program>,
3671    ) -> Result<AstNodeRef, KclError> {
3672        let sketch_id = sketch;
3673
3674        // Map the runtime objects back to variable names.
3675        let first_arg_ast = match horizontal {
3676            Horizontal::Line { line } => {
3677                let line_object = self
3678                    .scene_graph
3679                    .objects
3680                    .get(line.0)
3681                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line:?}")))?;
3682                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3683                    let kind = line_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(_) = line_segment else {
3689                    return Err(KclError::refactor(format!(
3690                        "Only lines can be made horizontal, but you selected {}",
3691                        line_segment.human_friendly_kind_with_article(),
3692                    )));
3693                };
3694                get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)?
3695            }
3696            Horizontal::Points { points } => {
3697                let point_asts = points
3698                    .iter()
3699                    .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
3700                    .collect::<Result<Vec<_>, _>>()?;
3701                ast::ArrayExpression::new(point_asts).into()
3702            }
3703        };
3704
3705        // Create the horizontal() call using shared helper.
3706        let horizontal_ast = create_horizontal_ast(first_arg_ast);
3707
3708        // Add the line to the AST of the sketch block.
3709        let (sketch_block_ref, _) = self.mutate_ast(
3710            new_ast,
3711            sketch_id,
3712            AstMutateCommand::AddSketchBlockExprStmt { expr: horizontal_ast },
3713        )?;
3714        Ok(sketch_block_ref)
3715    }
3716
3717    async fn add_lines_equal_length(
3718        &mut self,
3719        sketch: ObjectId,
3720        lines_equal_length: LinesEqualLength,
3721        new_ast: &mut ast::Node<ast::Program>,
3722    ) -> Result<AstNodeRef, KclError> {
3723        if lines_equal_length.lines.len() < 2 {
3724            return Err(KclError::refactor(format!(
3725                "Lines equal length constraint must have at least 2 lines, got {}",
3726                lines_equal_length.lines.len()
3727            )));
3728        };
3729
3730        let sketch_id = sketch;
3731
3732        // Map the runtime objects back to variable names.
3733        let line_asts = lines_equal_length
3734            .lines
3735            .iter()
3736            .map(|line_id| {
3737                let line_object = self
3738                    .scene_graph
3739                    .objects
3740                    .get(line_id.0)
3741                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3742                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3743                    let kind = line_object.kind.human_friendly_kind_with_article();
3744                    return Err(KclError::refactor(format!(
3745                        "This constraint only works on Segments, but you selected {kind}"
3746                    )));
3747                };
3748                let Segment::Line(_) = line_segment else {
3749                    let kind = line_segment.human_friendly_kind_with_article();
3750                    return Err(KclError::refactor(format!(
3751                        "Only lines can be made equal length, but you selected {kind}"
3752                    )));
3753                };
3754
3755                get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)
3756            })
3757            .collect::<Result<Vec<_>, _>>()?;
3758
3759        // Create the equalLength() call using shared helper.
3760        let equal_length_ast = create_equal_length_ast(line_asts);
3761
3762        // Add the constraint to the AST of the sketch block.
3763        let (sketch_block_ref, _) = self.mutate_ast(
3764            new_ast,
3765            sketch_id,
3766            AstMutateCommand::AddSketchBlockExprStmt { expr: equal_length_ast },
3767        )?;
3768        Ok(sketch_block_ref)
3769    }
3770
3771    fn equal_radius_segment_id_to_ast_reference(
3772        &mut self,
3773        segment_id: ObjectId,
3774        new_ast: &mut ast::Node<ast::Program>,
3775    ) -> Result<ast::Expr, KclError> {
3776        let segment_object = self
3777            .scene_graph
3778            .objects
3779            .get(segment_id.0)
3780            .ok_or_else(|| KclError::refactor(format!("Segment not found: {segment_id:?}")))?;
3781        let ObjectKind::Segment { segment } = &segment_object.kind else {
3782            return Err(KclError::refactor(format!(
3783                "Object is not a segment, it was {}",
3784                segment_object.kind.human_friendly_kind_with_article()
3785            )));
3786        };
3787
3788        let ref_type = match segment {
3789            Segment::Arc(_) => ARC_VARIABLE,
3790            Segment::Circle(_) => CIRCLE_VARIABLE,
3791            _ => {
3792                return Err(KclError::refactor(format!(
3793                    "equalRadius supports only arc/circle segments, got {}",
3794                    segment.human_friendly_kind_with_article()
3795                )));
3796            }
3797        };
3798
3799        get_or_insert_ast_reference(new_ast, &segment_object.source, ref_type, None)
3800    }
3801
3802    fn symmetric_input_id_to_ast_reference(
3803        &mut self,
3804        segment_id: ObjectId,
3805        new_ast: &mut ast::Node<ast::Program>,
3806    ) -> Result<ast::Expr, KclError> {
3807        let segment_object = self
3808            .scene_graph
3809            .objects
3810            .get(segment_id.0)
3811            .ok_or_else(|| KclError::refactor(format!("Segment not found: {segment_id:?}")))?;
3812        let ObjectKind::Segment { segment } = &segment_object.kind else {
3813            return Err(KclError::refactor(format!(
3814                "Object is not a segment, it was {}",
3815                segment_object.kind.human_friendly_kind_with_article()
3816            )));
3817        };
3818
3819        match segment {
3820            Segment::Point(_) => self.point_id_to_ast_reference(segment_id, new_ast),
3821            Segment::Line(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, LINE_VARIABLE, None),
3822            Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, ARC_VARIABLE, None),
3823            Segment::Circle(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, CIRCLE_VARIABLE, None),
3824        }
3825    }
3826
3827    fn symmetric_axis_id_to_ast_reference(
3828        &mut self,
3829        segment_id: ObjectId,
3830        new_ast: &mut ast::Node<ast::Program>,
3831    ) -> Result<ast::Expr, KclError> {
3832        let segment_object = self
3833            .scene_graph
3834            .objects
3835            .get(segment_id.0)
3836            .ok_or_else(|| KclError::refactor(format!("Axis segment not found: {segment_id:?}")))?;
3837        let ObjectKind::Segment { segment } = &segment_object.kind else {
3838            return Err(KclError::refactor(format!(
3839                "Object is not a segment, it was {}",
3840                segment_object.kind.human_friendly_kind_with_article()
3841            )));
3842        };
3843        match segment {
3844            Segment::Line(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, LINE_VARIABLE, None),
3845            _ => Err(KclError::refactor(format!(
3846                "Symmetric axis must be a line, got {}",
3847                segment.human_friendly_kind_with_article()
3848            ))),
3849        }
3850    }
3851
3852    async fn add_parallel(
3853        &mut self,
3854        sketch: ObjectId,
3855        parallel: Parallel,
3856        new_ast: &mut ast::Node<ast::Program>,
3857    ) -> Result<AstNodeRef, KclError> {
3858        if parallel.lines.len() < 2 {
3859            return Err(KclError::refactor(format!(
3860                "Parallel constraint must have at least 2 lines, got {}",
3861                parallel.lines.len()
3862            )));
3863        };
3864
3865        let sketch_id = sketch;
3866
3867        let line_asts = parallel
3868            .lines
3869            .iter()
3870            .map(|line_id| {
3871                let line_object = self
3872                    .scene_graph
3873                    .objects
3874                    .get(line_id.0)
3875                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3876                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3877                    let kind = line_object.kind.human_friendly_kind_with_article();
3878                    return Err(KclError::refactor(format!(
3879                        "This constraint only works on Segments, but you selected {kind}"
3880                    )));
3881                };
3882                let Segment::Line(_) = line_segment else {
3883                    let kind = line_segment.human_friendly_kind_with_article();
3884                    return Err(KclError::refactor(format!(
3885                        "Only lines can be made parallel, but you selected {kind}"
3886                    )));
3887                };
3888
3889                get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)
3890            })
3891            .collect::<Result<Vec<_>, _>>()?;
3892
3893        let call_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3894            callee: ast::Node::no_src(ast_sketch2_name(LinesAtAngleKind::Parallel.to_function_name())),
3895            unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
3896                ast::ArrayExpression {
3897                    elements: line_asts,
3898                    digest: None,
3899                    non_code_meta: Default::default(),
3900                },
3901            )))),
3902            arguments: Default::default(),
3903            digest: None,
3904            non_code_meta: Default::default(),
3905        })));
3906
3907        let (sketch_block_ref, _) = self.mutate_ast(
3908            new_ast,
3909            sketch_id,
3910            AstMutateCommand::AddSketchBlockExprStmt { expr: call_ast },
3911        )?;
3912        Ok(sketch_block_ref)
3913    }
3914
3915    async fn add_perpendicular(
3916        &mut self,
3917        sketch: ObjectId,
3918        perpendicular: Perpendicular,
3919        new_ast: &mut ast::Node<ast::Program>,
3920    ) -> Result<AstNodeRef, KclError> {
3921        self.add_lines_at_angle_constraint(sketch, LinesAtAngleKind::Perpendicular, perpendicular.lines, new_ast)
3922            .await
3923    }
3924
3925    async fn add_lines_at_angle_constraint(
3926        &mut self,
3927        sketch: ObjectId,
3928        angle_kind: LinesAtAngleKind,
3929        lines: Vec<ObjectId>,
3930        new_ast: &mut ast::Node<ast::Program>,
3931    ) -> Result<AstNodeRef, KclError> {
3932        let &[line0_id, line1_id] = lines.as_slice() else {
3933            return Err(KclError::refactor(format!(
3934                "{} constraint must have exactly 2 lines, got {}",
3935                angle_kind.to_function_name(),
3936                lines.len()
3937            )));
3938        };
3939
3940        let sketch_id = sketch;
3941
3942        // Map the runtime objects back to variable names.
3943        let line0_object = self
3944            .scene_graph
3945            .objects
3946            .get(line0_id.0)
3947            .ok_or_else(|| KclError::refactor(format!("Line not found: {line0_id:?}")))?;
3948        let ObjectKind::Segment { segment: line0_segment } = &line0_object.kind else {
3949            let kind = line0_object.kind.human_friendly_kind_with_article();
3950            return Err(KclError::refactor(format!(
3951                "This constraint only works on Segments, but you selected {kind}"
3952            )));
3953        };
3954        let Segment::Line(_) = line0_segment else {
3955            return Err(KclError::refactor(format!(
3956                "Only lines can be made {}, but you selected {}",
3957                angle_kind.to_function_name(),
3958                line0_segment.human_friendly_kind_with_article(),
3959            )));
3960        };
3961        let line0_ast = get_or_insert_ast_reference(new_ast, &line0_object.source.clone(), LINE_VARIABLE, None)?;
3962
3963        let line1_object = self
3964            .scene_graph
3965            .objects
3966            .get(line1_id.0)
3967            .ok_or_else(|| KclError::refactor(format!("Line not found: {line1_id:?}")))?;
3968        let ObjectKind::Segment { segment: line1_segment } = &line1_object.kind else {
3969            let kind = line1_object.kind.human_friendly_kind_with_article();
3970            return Err(KclError::refactor(format!(
3971                "This constraint only works on Segments, but you selected {kind}"
3972            )));
3973        };
3974        let Segment::Line(_) = line1_segment else {
3975            return Err(KclError::refactor(format!(
3976                "Only lines can be made {}, but you selected {}",
3977                angle_kind.to_function_name(),
3978                line1_segment.human_friendly_kind_with_article(),
3979            )));
3980        };
3981        let line1_ast = get_or_insert_ast_reference(new_ast, &line1_object.source.clone(), LINE_VARIABLE, None)?;
3982
3983        // Create the parallel() or perpendicular() call.
3984        let call_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3985            callee: ast::Node::no_src(ast_sketch2_name(angle_kind.to_function_name())),
3986            unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
3987                ast::ArrayExpression {
3988                    elements: vec![line0_ast, line1_ast],
3989                    digest: None,
3990                    non_code_meta: Default::default(),
3991                },
3992            )))),
3993            arguments: Default::default(),
3994            digest: None,
3995            non_code_meta: Default::default(),
3996        })));
3997
3998        // Add the constraint to the AST of the sketch block.
3999        let (sketch_block_ref, _) = self.mutate_ast(
4000            new_ast,
4001            sketch_id,
4002            AstMutateCommand::AddSketchBlockExprStmt { expr: call_ast },
4003        )?;
4004        Ok(sketch_block_ref)
4005    }
4006
4007    async fn add_vertical(
4008        &mut self,
4009        sketch: ObjectId,
4010        vertical: Vertical,
4011        new_ast: &mut ast::Node<ast::Program>,
4012    ) -> Result<AstNodeRef, KclError> {
4013        let sketch_id = sketch;
4014
4015        let first_arg_ast = match vertical {
4016            Vertical::Line { line } => {
4017                // Map the runtime objects back to variable names.
4018                let line_object = self
4019                    .scene_graph
4020                    .objects
4021                    .get(line.0)
4022                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line:?}")))?;
4023                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4024                    let kind = line_object.kind.human_friendly_kind_with_article();
4025                    return Err(KclError::refactor(format!(
4026                        "This constraint only works on Segments, but you selected {kind}"
4027                    )));
4028                };
4029                let Segment::Line(_) = line_segment else {
4030                    return Err(KclError::refactor(format!(
4031                        "Only lines can be made vertical, but you selected {}",
4032                        line_segment.human_friendly_kind_with_article()
4033                    )));
4034                };
4035                get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)?
4036            }
4037            Vertical::Points { points } => {
4038                let point_asts = points
4039                    .iter()
4040                    .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
4041                    .collect::<Result<Vec<_>, _>>()?;
4042                ast::ArrayExpression::new(point_asts).into()
4043            }
4044        };
4045
4046        // Create the vertical() call using shared helper.
4047        let vertical_ast = create_vertical_ast(first_arg_ast);
4048
4049        // Add the line to the AST of the sketch block.
4050        let (sketch_block_ref, _) = self.mutate_ast(
4051            new_ast,
4052            sketch_id,
4053            AstMutateCommand::AddSketchBlockExprStmt { expr: vertical_ast },
4054        )?;
4055        Ok(sketch_block_ref)
4056    }
4057
4058    async fn execute_after_add_constraint(
4059        &mut self,
4060        ctx: &ExecutorContext,
4061        sketch_id: ObjectId,
4062        #[cfg_attr(not(feature = "artifact-graph"), allow(unused_variables))] sketch_block_ref: AstNodeRef,
4063        new_ast: &mut ast::Node<ast::Program>,
4064    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
4065        // Convert to string source to create real source ranges.
4066        let new_source = source_from_ast(new_ast);
4067        // Parse the new KCL source.
4068        let (new_program, errors) = Program::parse(&new_source)
4069            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
4070        if !errors.is_empty() {
4071            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
4072                "Error parsing KCL source after adding constraint: {errors:?}"
4073            ))));
4074        }
4075        let Some(new_program) = new_program else {
4076            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
4077                "No AST produced after adding constraint".to_string(),
4078            )));
4079        };
4080        #[cfg(feature = "artifact-graph")]
4081        let constraint_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
4082            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
4083                "Source range of new constraint not found in sketch block: {sketch_block_ref:?}; {err:?}"
4084            )))
4085        })?;
4086
4087        // Truncate after the sketch block for mock execution.
4088        // Use a clone so we don't mutate new_program yet
4089        let mut truncated_program = new_program.clone();
4090        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
4091            .map_err(KclErrorWithOutputs::no_outputs)?;
4092
4093        // Execute - if this fails, we haven't modified self yet, so state is safe
4094        let outcome = ctx
4095            .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch_id))
4096            .await?;
4097
4098        #[cfg(not(feature = "artifact-graph"))]
4099        let new_object_ids = Vec::new();
4100        #[cfg(feature = "artifact-graph")]
4101        let new_object_ids = {
4102            // Extract the constraint ID from the execution outcome using source_range_to_object
4103            let constraint_id = outcome
4104                .source_range_to_object
4105                .get(&constraint_node_ref.range)
4106                .copied()
4107                .ok_or_else(|| {
4108                    KclErrorWithOutputs::from_error_outcome(
4109                        KclError::refactor(format!("Source range of constraint not found: {constraint_node_ref:?}")),
4110                        outcome.clone(),
4111                    )
4112                })?;
4113            vec![constraint_id]
4114        };
4115
4116        // Only now, after all operations succeeded, update self.program
4117        // This ensures state is only modified if everything succeeds
4118        self.program = new_program;
4119
4120        // Uses MockConfig::default() which has freedom_analysis: true
4121        let outcome = self.update_state_after_exec(outcome, true);
4122
4123        let src_delta = SourceDelta { text: new_source };
4124        let scene_graph_delta = SceneGraphDelta {
4125            new_graph: self.scene_graph.clone(),
4126            invalidates_ids: false,
4127            new_objects: new_object_ids,
4128            exec_outcome: outcome,
4129        };
4130        Ok((src_delta, scene_graph_delta))
4131    }
4132
4133    // Find constraints that reference the given segments.
4134    fn segment_will_be_deleted(&self, segment_id: ObjectId, segment_ids_set: &AhashIndexSet<ObjectId>) -> bool {
4135        if segment_ids_set.contains(&segment_id) {
4136            return true;
4137        }
4138
4139        let Some(segment_object) = self.scene_graph.objects.get(segment_id.0) else {
4140            return false;
4141        };
4142        let ObjectKind::Segment { segment } = &segment_object.kind else {
4143            return false;
4144        };
4145        let Segment::Point(point) = segment else {
4146            return false;
4147        };
4148
4149        point.owner.is_some_and(|owner_id| segment_ids_set.contains(&owner_id))
4150    }
4151
4152    fn remaining_constraint_segments(
4153        &self,
4154        segments: &[ConstraintSegment],
4155        segment_ids_set: &AhashIndexSet<ObjectId>,
4156    ) -> Vec<ConstraintSegment> {
4157        segments
4158            .iter()
4159            .copied()
4160            .filter(|segment| match segment {
4161                ConstraintSegment::Origin(_) => true,
4162                ConstraintSegment::Segment(segment_id) => !self.segment_will_be_deleted(*segment_id, segment_ids_set),
4163            })
4164            .collect()
4165    }
4166
4167    fn find_referenced_constraints(
4168        &self,
4169        sketch_id: ObjectId,
4170        segment_ids_set: &AhashIndexSet<ObjectId>,
4171    ) -> Result<AhashIndexSet<ObjectId>, KclError> {
4172        // Look up the sketch.
4173        let sketch_object = self
4174            .scene_graph
4175            .objects
4176            .get(sketch_id.0)
4177            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch_id:?}")))?;
4178        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
4179            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
4180        };
4181        let mut constraint_ids_set = AhashIndexSet::default();
4182        for constraint_id in &sketch.constraints {
4183            let constraint_object = self
4184                .scene_graph
4185                .objects
4186                .get(constraint_id.0)
4187                .ok_or_else(|| KclError::refactor(format!("Constraint not found: {constraint_id:?}")))?;
4188            let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
4189                return Err(KclError::refactor(format!(
4190                    "Object is not a constraint, it is {}",
4191                    constraint_object.kind.human_friendly_kind_with_article()
4192                )));
4193            };
4194            let depends_on_segment = match constraint {
4195                Constraint::Coincident(c) => c
4196                    .segment_ids()
4197                    .any(|seg_id| self.segment_will_be_deleted(seg_id, segment_ids_set)),
4198                Constraint::Distance(d) => d
4199                    .point_ids()
4200                    .any(|pt_id| self.segment_will_be_deleted(pt_id, segment_ids_set)),
4201                Constraint::Fixed(fixed) => fixed
4202                    .points
4203                    .iter()
4204                    .any(|fixed_point| self.segment_will_be_deleted(fixed_point.point, segment_ids_set)),
4205                Constraint::Radius(r) => self.segment_will_be_deleted(r.arc, segment_ids_set),
4206                Constraint::Diameter(d) => self.segment_will_be_deleted(d.arc, segment_ids_set),
4207                Constraint::EqualRadius(equal_radius) => equal_radius
4208                    .input
4209                    .iter()
4210                    .any(|seg_id| self.segment_will_be_deleted(*seg_id, segment_ids_set)),
4211                Constraint::HorizontalDistance(d) => d
4212                    .point_ids()
4213                    .any(|pt_id| self.segment_will_be_deleted(pt_id, segment_ids_set)),
4214                Constraint::VerticalDistance(d) => d
4215                    .point_ids()
4216                    .any(|pt_id| self.segment_will_be_deleted(pt_id, segment_ids_set)),
4217                Constraint::Horizontal(h) => match h {
4218                    Horizontal::Line { line } => self.segment_will_be_deleted(*line, segment_ids_set),
4219                    Horizontal::Points { points } => points.iter().any(|point| match point {
4220                        ConstraintSegment::Segment(point) => self.segment_will_be_deleted(*point, segment_ids_set),
4221                        ConstraintSegment::Origin(_) => false,
4222                    }),
4223                },
4224                Constraint::Vertical(v) => match v {
4225                    Vertical::Line { line } => self.segment_will_be_deleted(*line, segment_ids_set),
4226                    Vertical::Points { points } => points.iter().any(|point| match point {
4227                        ConstraintSegment::Segment(point) => self.segment_will_be_deleted(*point, segment_ids_set),
4228                        ConstraintSegment::Origin(_) => false,
4229                    }),
4230                },
4231                Constraint::LinesEqualLength(lines_equal_length) => lines_equal_length
4232                    .lines
4233                    .iter()
4234                    .any(|line_id| self.segment_will_be_deleted(*line_id, segment_ids_set)),
4235                Constraint::Midpoint(midpoint) => {
4236                    self.segment_will_be_deleted(midpoint.segment, segment_ids_set)
4237                        || self.segment_will_be_deleted(midpoint.point, segment_ids_set)
4238                }
4239                Constraint::Parallel(parallel) => parallel
4240                    .lines
4241                    .iter()
4242                    .any(|line_id| self.segment_will_be_deleted(*line_id, segment_ids_set)),
4243                Constraint::Perpendicular(perpendicular) => perpendicular
4244                    .lines
4245                    .iter()
4246                    .any(|line_id| self.segment_will_be_deleted(*line_id, segment_ids_set)),
4247                Constraint::Angle(angle) => angle
4248                    .lines
4249                    .iter()
4250                    .any(|line_id| self.segment_will_be_deleted(*line_id, segment_ids_set)),
4251                Constraint::Symmetric(symmetric) => {
4252                    self.segment_will_be_deleted(symmetric.axis, segment_ids_set)
4253                        || symmetric
4254                            .input
4255                            .iter()
4256                            .any(|seg_id| self.segment_will_be_deleted(*seg_id, segment_ids_set))
4257                }
4258                Constraint::Tangent(tangent) => tangent
4259                    .input
4260                    .iter()
4261                    .any(|seg_id| self.segment_will_be_deleted(*seg_id, segment_ids_set)),
4262            };
4263            if depends_on_segment {
4264                constraint_ids_set.insert(*constraint_id);
4265            }
4266        }
4267        Ok(constraint_ids_set)
4268    }
4269
4270    fn update_state_after_exec(&mut self, outcome: ExecOutcome, freedom_analysis_ran: bool) -> ExecOutcome {
4271        #[cfg(not(feature = "artifact-graph"))]
4272        {
4273            let _ = freedom_analysis_ran; // Only used when artifact-graph feature is enabled
4274            outcome
4275        }
4276        #[cfg(feature = "artifact-graph")]
4277        {
4278            let mut outcome = outcome;
4279            let mut new_objects = std::mem::take(&mut outcome.scene_objects);
4280
4281            if freedom_analysis_ran {
4282                // When freedom analysis ran, replace the cache entirely with new values
4283                // Don't merge with old values since IDs might have changed
4284                self.point_freedom_cache.clear();
4285                for new_obj in &new_objects {
4286                    if let ObjectKind::Segment {
4287                        segment: crate::front::Segment::Point(point),
4288                    } = &new_obj.kind
4289                    {
4290                        self.point_freedom_cache.insert(new_obj.id, point.freedom);
4291                    }
4292                }
4293                add_wall_and_cap_face_objects(&mut new_objects, &outcome.artifact_graph);
4294                // Objects are already correct from the analysis, just use them as-is
4295                self.scene_graph.objects = new_objects;
4296            } else {
4297                // When freedom analysis didn't run, preserve old values and merge
4298                // Before replacing objects, extract and store freedom values from old objects
4299                for old_obj in &self.scene_graph.objects {
4300                    if let ObjectKind::Segment {
4301                        segment: crate::front::Segment::Point(point),
4302                    } = &old_obj.kind
4303                    {
4304                        self.point_freedom_cache.insert(old_obj.id, point.freedom);
4305                    }
4306                }
4307
4308                // Update objects, preserving stored freedom values when new is Free (might be default)
4309                let mut updated_objects = Vec::with_capacity(new_objects.len());
4310                for new_obj in new_objects {
4311                    let mut obj = new_obj;
4312                    if let ObjectKind::Segment {
4313                        segment: crate::front::Segment::Point(point),
4314                    } = &mut obj.kind
4315                    {
4316                        let new_freedom = point.freedom;
4317                        // When freedom_analysis=false, new values are defaults (Free).
4318                        // Only preserve cached values when new is Free (indicating it's a default, not from analysis).
4319                        // If new is NOT Free, use the new value (it came from somewhere else, maybe conflict detection).
4320                        // Never preserve Conflict from cache - conflicts are transient and should only be set
4321                        // when there are actually unsatisfied constraints.
4322                        match new_freedom {
4323                            Freedom::Free => {
4324                                match self.point_freedom_cache.get(&obj.id).copied() {
4325                                    Some(Freedom::Conflict) => {
4326                                        // Don't preserve Conflict - conflicts are transient
4327                                        // Keep it as Free
4328                                    }
4329                                    Some(Freedom::Fixed) => {
4330                                        // Preserve Fixed cached value
4331                                        point.freedom = Freedom::Fixed;
4332                                    }
4333                                    Some(Freedom::Free) => {
4334                                        // If stored is also Free, keep Free (no change needed)
4335                                    }
4336                                    None => {
4337                                        // If no cached value, keep Free (default)
4338                                    }
4339                                }
4340                            }
4341                            Freedom::Fixed => {
4342                                // Use new value (already set)
4343                            }
4344                            Freedom::Conflict => {
4345                                // Use new value (already set)
4346                            }
4347                        }
4348                        // Store the new freedom value (even if it's Free, so we know it was set)
4349                        self.point_freedom_cache.insert(obj.id, point.freedom);
4350                    }
4351                    updated_objects.push(obj);
4352                }
4353
4354                add_wall_and_cap_face_objects(&mut updated_objects, &outcome.artifact_graph);
4355                self.scene_graph.objects = updated_objects;
4356            }
4357            outcome
4358        }
4359    }
4360
4361    fn mutate_ast(
4362        &mut self,
4363        ast: &mut ast::Node<ast::Program>,
4364        object_id: ObjectId,
4365        command: AstMutateCommand,
4366    ) -> Result<(AstNodeRef, AstMutateCommandReturn), KclError> {
4367        let sketch_object = self
4368            .scene_graph
4369            .objects
4370            .get(object_id.0)
4371            .ok_or_else(|| KclError::refactor(format!("Object not found: {object_id:?}")))?;
4372        mutate_ast_node_by_source_ref(ast, &sketch_object.source, command)
4373    }
4374}
4375
4376fn sketch_block_ref_from_id(scene_graph: &SceneGraph, sketch_id: ObjectId) -> Result<AstNodeRef, KclError> {
4377    // Look up existing sketch.
4378    let sketch_object = scene_graph
4379        .objects
4380        .get(sketch_id.0)
4381        .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch_id:?}")))?;
4382    let ObjectKind::Sketch(_) = &sketch_object.kind else {
4383        return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
4384    };
4385    expect_single_node_ref(sketch_object)
4386}
4387
4388fn expect_single_node_ref(object: &Object) -> Result<AstNodeRef, KclError> {
4389    match &object.source {
4390        SourceRef::Simple { range, node_path } => Ok(AstNodeRef {
4391            range: *range,
4392            node_path: node_path.clone(),
4393        }),
4394        SourceRef::BackTrace { ranges } => {
4395            let [range] = ranges.as_slice() else {
4396                return Err(KclError::refactor(format!(
4397                    "Expected single location in SourceRef, got {}; ranges={ranges:#?}",
4398                    ranges.len()
4399                )));
4400            };
4401            Ok(AstNodeRef {
4402                range: range.0,
4403                node_path: range.1.clone(),
4404            })
4405        }
4406    }
4407}
4408
4409/// This is a deprecated fall-back implementation. Prefer
4410/// [`only_sketch_block()`] to avoid reliance on source ranges.
4411fn only_sketch_block_from_range(
4412    ast: &mut ast::Node<ast::Program>,
4413    sketch_block_range: SourceRange,
4414    edit_kind: ChangeKind,
4415) -> Result<(), KclError> {
4416    let r1 = sketch_block_range;
4417    let matches_range = |r2: SourceRange| -> bool {
4418        // We may have added items to the sketch block, so the end may not be an
4419        // exact match.
4420        match edit_kind {
4421            ChangeKind::Add => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() <= r2.end(),
4422            // For edit, we don't know whether it grew or shrank.
4423            ChangeKind::Edit => r1.module_id() == r2.module_id() && r1.start() == r2.start(),
4424            ChangeKind::Delete => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() >= r2.end(),
4425            // No edit should be an exact match.
4426            ChangeKind::None => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() == r2.end(),
4427        }
4428    };
4429    let mut found = false;
4430    for item in ast.body.iter_mut() {
4431        match item {
4432            ast::BodyItem::ImportStatement(_) => {}
4433            ast::BodyItem::ExpressionStatement(node) => {
4434                if matches_range(SourceRange::from(&*node))
4435                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.expression
4436                {
4437                    sketch_block.is_being_edited = true;
4438                    found = true;
4439                    break;
4440                }
4441            }
4442            ast::BodyItem::VariableDeclaration(node) => {
4443                if matches_range(SourceRange::from(&node.declaration.init))
4444                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.declaration.init
4445                {
4446                    sketch_block.is_being_edited = true;
4447                    found = true;
4448                    break;
4449                }
4450            }
4451            ast::BodyItem::TypeDeclaration(_) => {}
4452            ast::BodyItem::ReturnStatement(node) => {
4453                if matches_range(SourceRange::from(&node.argument))
4454                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.argument
4455                {
4456                    sketch_block.is_being_edited = true;
4457                    found = true;
4458                    break;
4459                }
4460            }
4461        }
4462    }
4463    if !found {
4464        return Err(KclError::refactor(format!(
4465            "Sketch block source range not found in AST: {sketch_block_range:?}, edit_kind={edit_kind:?}"
4466        )));
4467    }
4468
4469    Ok(())
4470}
4471
4472fn only_sketch_block(
4473    ast: &mut ast::Node<ast::Program>,
4474    sketch_block_ref: &AstNodeRef,
4475    edit_kind: ChangeKind,
4476) -> Result<(), KclError> {
4477    let Some(target_node_path) = &sketch_block_ref.node_path else {
4478        #[cfg(target_arch = "wasm32")]
4479        web_sys::console::warn_1(
4480            &format!(
4481                "only_sketch_block: target sketch block ref doesn't have node path; sketch_block_ref={:#?}, edit_kind={edit_kind:#?}",
4482                &sketch_block_ref
4483            )
4484            .into(),
4485        );
4486        return only_sketch_block_from_range(ast, sketch_block_ref.range, edit_kind);
4487    };
4488    let mut found = false;
4489    for item in ast.body.iter_mut() {
4490        match item {
4491            ast::BodyItem::ImportStatement(_) => {}
4492            ast::BodyItem::ExpressionStatement(node) => {
4493                // Check the statement.
4494                if let Some(node_path) = &node.node_path
4495                    && node_path == target_node_path
4496                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.expression
4497                {
4498                    sketch_block.is_being_edited = true;
4499                    found = true;
4500                    break;
4501                }
4502                // Check the expression.
4503                if let Some(node_path) = node.expression.node_path()
4504                    && node_path == target_node_path
4505                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.expression
4506                {
4507                    sketch_block.is_being_edited = true;
4508                    found = true;
4509                    break;
4510                }
4511            }
4512            ast::BodyItem::VariableDeclaration(node) => {
4513                if let Some(node_path) = node.declaration.init.node_path()
4514                    && node_path == target_node_path
4515                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.declaration.init
4516                {
4517                    sketch_block.is_being_edited = true;
4518                    found = true;
4519                    break;
4520                }
4521            }
4522            ast::BodyItem::TypeDeclaration(_) => {}
4523            ast::BodyItem::ReturnStatement(node) => {
4524                if let Some(node_path) = node.argument.node_path()
4525                    && node_path == target_node_path
4526                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.argument
4527                {
4528                    sketch_block.is_being_edited = true;
4529                    found = true;
4530                    break;
4531                }
4532            }
4533        }
4534    }
4535    if !found {
4536        return Err(KclError::refactor(format!(
4537            "Sketch block node path not found in AST: {sketch_block_ref:?}, edit_kind={edit_kind:?}"
4538        )));
4539    }
4540
4541    Ok(())
4542}
4543
4544fn sketch_on_ast_expr(
4545    ast: &mut ast::Node<ast::Program>,
4546    scene_graph: &SceneGraph,
4547    on: &Plane,
4548) -> Result<ast::Expr, KclError> {
4549    match on {
4550        Plane::Default(name) => Ok(default_plane_ast_expr(*name)),
4551        Plane::Object(object_id) => {
4552            let on_object = scene_graph
4553                .objects
4554                .get(object_id.0)
4555                .ok_or_else(|| KclError::refactor(format!("Sketch plane object not found: {object_id:?}")))?;
4556            #[cfg(feature = "artifact-graph")]
4557            {
4558                if let Some(face_expr) = sketch_face_of_scene_object_ast_expr(ast, on_object)? {
4559                    return Ok(face_expr);
4560                }
4561            }
4562            get_or_insert_ast_reference(ast, &on_object.source, "plane", None)
4563        }
4564    }
4565}
4566
4567#[cfg(feature = "artifact-graph")]
4568fn sketch_face_of_scene_object_ast_expr(
4569    ast: &mut ast::Node<ast::Program>,
4570    on_object: &crate::front::Object,
4571) -> Result<Option<ast::Expr>, KclError> {
4572    let SourceRef::BackTrace { ranges } = &on_object.source else {
4573        return Ok(None);
4574    };
4575
4576    match &on_object.kind {
4577        ObjectKind::Wall(_) => {
4578            let [sweep_range, segment_range] = ranges.as_slice() else {
4579                return Err(KclError::refactor(format!(
4580                    "Expected wall source metadata to have 2 ranges, got {}; artifact_id={:?}",
4581                    ranges.len(),
4582                    on_object.artifact_id
4583                )));
4584            };
4585            let sweep_ref = get_or_insert_ast_reference(
4586                ast,
4587                &SourceRef::Simple {
4588                    range: sweep_range.0,
4589                    node_path: sweep_range.1.clone(),
4590                },
4591                "solid",
4592                None,
4593            )?;
4594            let ast::Expr::Name(solid_name_expr) = sweep_ref else {
4595                return Err(KclError::refactor(format!(
4596                    "Could not resolve sweep reference for selected wall: artifact_id={:?}",
4597                    on_object.artifact_id
4598                )));
4599            };
4600            let solid_name = solid_name_expr.name.name.clone();
4601            let solid_expr = ast_name_expr(solid_name.clone());
4602            let segment_ref = get_or_insert_ast_reference(
4603                ast,
4604                &SourceRef::Simple {
4605                    range: segment_range.0,
4606                    node_path: segment_range.1.clone(),
4607                },
4608                LINE_VARIABLE,
4609                None,
4610            )?;
4611
4612            let face_expr = if let Some(region_name) = region_name_from_sweep_variable(ast, &solid_name) {
4613                let ast::Expr::Name(segment_name_expr) = segment_ref else {
4614                    return Err(KclError::refactor(format!(
4615                        "Could not resolve source segment reference for selected region wall: artifact_id={:?}",
4616                        on_object.artifact_id
4617                    )));
4618                };
4619                create_member_expression(
4620                    create_member_expression(ast_name_expr(region_name), "tags"),
4621                    &segment_name_expr.name.name,
4622                )
4623            } else {
4624                segment_ref
4625            };
4626
4627            Ok(Some(create_face_of_ast(solid_expr, face_expr)))
4628        }
4629        ObjectKind::Cap(cap) => {
4630            let [range] = ranges.as_slice() else {
4631                return Err(KclError::refactor(format!(
4632                    "Expected cap source metadata to have 1 range, got {}; artifact_id={:?}",
4633                    ranges.len(),
4634                    on_object.artifact_id
4635                )));
4636            };
4637            let sweep_ref = get_or_insert_ast_reference(
4638                ast,
4639                &SourceRef::Simple {
4640                    range: range.0,
4641                    node_path: range.1.clone(),
4642                },
4643                "solid",
4644                None,
4645            )?;
4646            let ast::Expr::Name(solid_name_expr) = sweep_ref else {
4647                return Err(KclError::refactor(format!(
4648                    "Could not resolve sweep reference for selected cap: artifact_id={:?}",
4649                    on_object.artifact_id
4650                )));
4651            };
4652            let solid_expr = ast_name_expr(solid_name_expr.name.name.clone());
4653            // TODO: change this to explicit tag references with tagStart/tagEnd mutations
4654            let face_expr = match cap.kind {
4655                crate::frontend::api::CapKind::Start => ast_name_expr("START".to_owned()),
4656                crate::frontend::api::CapKind::End => ast_name_expr("END".to_owned()),
4657            };
4658
4659            Ok(Some(create_face_of_ast(solid_expr, face_expr)))
4660        }
4661        _ => Ok(None),
4662    }
4663}
4664
4665#[cfg(feature = "artifact-graph")]
4666fn add_wall_and_cap_face_objects(scene_objects: &mut Vec<crate::front::Object>, artifact_graph: &ArtifactGraph) {
4667    let mut existing_artifact_ids = scene_objects
4668        .iter()
4669        .map(|object| object.artifact_id)
4670        .collect::<HashSet<_>>();
4671
4672    for artifact in artifact_graph.values() {
4673        match artifact {
4674            Artifact::Wall(wall) => {
4675                if existing_artifact_ids.contains(&wall.id) {
4676                    continue;
4677                }
4678
4679                let Some(segment) = artifact_graph.get(&wall.seg_id).and_then(|artifact| match artifact {
4680                    Artifact::Segment(segment) => Some(segment),
4681                    _ => None,
4682                }) else {
4683                    continue;
4684                };
4685                let Some(sweep) = artifact_graph.get(&wall.sweep_id).and_then(|artifact| match artifact {
4686                    Artifact::Sweep(sweep) => Some(sweep),
4687                    _ => None,
4688                }) else {
4689                    continue;
4690                };
4691                let source_segment = segment
4692                    .original_seg_id
4693                    .and_then(|original_seg_id| artifact_graph.get(&original_seg_id))
4694                    .and_then(|artifact| match artifact {
4695                        Artifact::Segment(segment) => Some(segment),
4696                        _ => None,
4697                    })
4698                    .unwrap_or(segment);
4699                let id = ObjectId(scene_objects.len());
4700                scene_objects.push(crate::front::Object {
4701                    id,
4702                    kind: ObjectKind::Wall(crate::frontend::api::Wall { id }),
4703                    label: Default::default(),
4704                    comments: Default::default(),
4705                    artifact_id: wall.id,
4706                    source: SourceRef::BackTrace {
4707                        ranges: vec![
4708                            (sweep.code_ref.range, Some(sweep.code_ref.node_path.clone())),
4709                            (
4710                                source_segment.code_ref.range,
4711                                Some(source_segment.code_ref.node_path.clone()),
4712                            ),
4713                        ],
4714                    },
4715                });
4716                existing_artifact_ids.insert(wall.id);
4717            }
4718            Artifact::Cap(cap) => {
4719                if existing_artifact_ids.contains(&cap.id) {
4720                    continue;
4721                }
4722
4723                let Some(sweep) = artifact_graph.get(&cap.sweep_id).and_then(|artifact| match artifact {
4724                    Artifact::Sweep(sweep) => Some(sweep),
4725                    _ => None,
4726                }) else {
4727                    continue;
4728                };
4729                let id = ObjectId(scene_objects.len());
4730                let kind = match cap.sub_type {
4731                    CapSubType::Start => crate::frontend::api::CapKind::Start,
4732                    CapSubType::End => crate::frontend::api::CapKind::End,
4733                };
4734                scene_objects.push(crate::front::Object {
4735                    id,
4736                    kind: ObjectKind::Cap(crate::frontend::api::Cap { id, kind }),
4737                    label: Default::default(),
4738                    comments: Default::default(),
4739                    artifact_id: cap.id,
4740                    source: SourceRef::BackTrace {
4741                        ranges: vec![(sweep.code_ref.range, Some(sweep.code_ref.node_path.clone()))],
4742                    },
4743                });
4744                existing_artifact_ids.insert(cap.id);
4745            }
4746            _ => {}
4747        }
4748    }
4749}
4750
4751fn default_plane_ast_expr(name: crate::engine::PlaneName) -> ast::Expr {
4752    use crate::engine::PlaneName;
4753
4754    match name {
4755        PlaneName::Xy => ast_name_expr("XY".to_owned()),
4756        PlaneName::Xz => ast_name_expr("XZ".to_owned()),
4757        PlaneName::Yz => ast_name_expr("YZ".to_owned()),
4758        PlaneName::NegXy => negated_plane_ast_expr("XY"),
4759        PlaneName::NegXz => negated_plane_ast_expr("XZ"),
4760        PlaneName::NegYz => negated_plane_ast_expr("YZ"),
4761    }
4762}
4763
4764fn negated_plane_ast_expr(name: &str) -> ast::Expr {
4765    ast::Expr::UnaryExpression(Box::new(ast::UnaryExpression::new(
4766        ast::UnaryOperator::Neg,
4767        ast::BinaryPart::Name(Box::new(ast_name(name.to_owned()))),
4768    )))
4769}
4770
4771#[cfg(feature = "artifact-graph")]
4772fn create_face_of_ast(solid_expr: ast::Expr, face_expr: ast::Expr) -> ast::Expr {
4773    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
4774        callee: ast::Node::no_src(ast_sketch2_name("faceOf")),
4775        unlabeled: Some(solid_expr),
4776        arguments: vec![ast::LabeledArg {
4777            label: Some(ast::Identifier::new("face")),
4778            arg: face_expr,
4779        }],
4780        digest: None,
4781        non_code_meta: Default::default(),
4782    })))
4783}
4784
4785#[cfg(feature = "artifact-graph")]
4786fn region_name_from_sweep_variable(ast: &ast::Node<ast::Program>, sweep_variable_name: &str) -> Option<String> {
4787    let ast::Definition::Variable(sweep_decl) = ast.get_variable(sweep_variable_name)? else {
4788        return None;
4789    };
4790    let ast::Expr::CallExpressionKw(sweep_call) = &sweep_decl.init else {
4791        return None;
4792    };
4793    if !matches!(
4794        sweep_call.callee.name.name.as_str(),
4795        "extrude" | "revolve" | "sweep" | "loft"
4796    ) {
4797        return None;
4798    }
4799    let ast::Expr::Name(region_name_expr) = sweep_call.unlabeled.as_ref()? else {
4800        return None;
4801    };
4802    let candidate = region_name_expr.name.name.clone();
4803    let ast::Definition::Variable(region_decl) = ast.get_variable(&candidate)? else {
4804        return None;
4805    };
4806    let ast::Expr::CallExpressionKw(region_call) = &region_decl.init else {
4807        return None;
4808    };
4809    if region_call.callee.name.name != "region" {
4810        return None;
4811    }
4812    Some(candidate)
4813}
4814
4815/// Return the AST expression referencing the variable at the given source ref.
4816/// If no such variable exists, insert a new variable declaration with the given
4817/// prefix.
4818///
4819/// This may return a complex expression referencing properties of the variable
4820/// (e.g., `line1.start`).
4821fn get_or_insert_ast_reference(
4822    ast: &mut ast::Node<ast::Program>,
4823    source_ref: &SourceRef,
4824    prefix: &str,
4825    property: Option<&str>,
4826) -> Result<ast::Expr, KclError> {
4827    let command = AstMutateCommand::AddVariableDeclaration {
4828        prefix: prefix.to_owned(),
4829    };
4830    let (_, ret) = mutate_ast_node_by_source_ref(ast, source_ref, command)?;
4831    let AstMutateCommandReturn::Name(var_name) = ret else {
4832        return Err(KclError::refactor(
4833            "Expected variable name returned from AddVariableDeclaration".to_owned(),
4834        ));
4835    };
4836    let var_expr = ast::Expr::Name(Box::new(ast::Name::new(&var_name)));
4837    let Some(property) = property else {
4838        // No property; just return the variable name.
4839        return Ok(var_expr);
4840    };
4841
4842    Ok(create_member_expression(var_expr, property))
4843}
4844
4845fn mutate_ast_node_by_source_ref(
4846    ast: &mut ast::Node<ast::Program>,
4847    source_ref: &SourceRef,
4848    command: AstMutateCommand,
4849) -> Result<(AstNodeRef, AstMutateCommandReturn), KclError> {
4850    let (source_range, node_path) = match source_ref {
4851        SourceRef::Simple { range, node_path } => (*range, node_path.clone()),
4852        SourceRef::BackTrace { ranges } => {
4853            let [range] = ranges.as_slice() else {
4854                return Err(KclError::refactor(format!(
4855                    "Expected single source ref, got {}; ranges={ranges:#?}",
4856                    ranges.len(),
4857                )));
4858            };
4859            (range.0, range.1.clone())
4860        }
4861    };
4862    let mut context = AstMutateContext {
4863        source_range,
4864        node_path,
4865        command,
4866        defined_names_stack: Default::default(),
4867    };
4868    let control = dfs_mut(ast, &mut context);
4869    match control {
4870        ControlFlow::Continue(_) => Err(KclError::refactor(
4871            "Could not find the KCL source for this edit. Try reloading the app, or update from code.".to_owned(),
4872        )),
4873        ControlFlow::Break(break_value) => break_value,
4874    }
4875}
4876
4877#[derive(Debug)]
4878struct AstMutateContext {
4879    source_range: SourceRange,
4880    node_path: Option<ast::NodePath>,
4881    command: AstMutateCommand,
4882    defined_names_stack: Vec<HashSet<String>>,
4883}
4884
4885#[derive(Debug)]
4886#[allow(clippy::large_enum_variant)]
4887enum AstMutateCommand {
4888    /// Add an expression statement to the sketch block.
4889    AddSketchBlockExprStmt {
4890        expr: ast::Expr,
4891    },
4892    /// Add a variable declaration to the sketch block (e.g. `line1 = line(...)`).
4893    AddSketchBlockVarDecl {
4894        prefix: String,
4895        expr: ast::Expr,
4896    },
4897    AddVariableDeclaration {
4898        prefix: String,
4899    },
4900    EditPoint {
4901        at: ast::Expr,
4902    },
4903    EditLine {
4904        start: ast::Expr,
4905        end: ast::Expr,
4906        construction: Option<bool>,
4907    },
4908    EditArc {
4909        start: ast::Expr,
4910        end: ast::Expr,
4911        center: ast::Expr,
4912        construction: Option<bool>,
4913    },
4914    EditCircle {
4915        start: ast::Expr,
4916        center: ast::Expr,
4917        construction: Option<bool>,
4918    },
4919    EditConstraintValue {
4920        value: ast::BinaryPart,
4921    },
4922    EditDistanceConstraintLabelPosition {
4923        label_position: ast::Expr,
4924    },
4925    EditCallUnlabeled {
4926        arg: ast::Expr,
4927    },
4928    #[cfg(feature = "artifact-graph")]
4929    EditVarInitialValue {
4930        value: Number,
4931    },
4932    DeleteNode,
4933}
4934
4935impl AstMutateCommand {
4936    fn needs_defined_names_stack(&self) -> bool {
4937        matches!(
4938            self,
4939            AstMutateCommand::AddSketchBlockVarDecl { .. } | AstMutateCommand::AddVariableDeclaration { .. }
4940        )
4941    }
4942}
4943
4944#[derive(Debug)]
4945enum AstMutateCommandReturn {
4946    None,
4947    Name(String),
4948}
4949
4950#[derive(Debug, Clone)]
4951struct AstNodeRef {
4952    range: SourceRange,
4953    node_path: Option<ast::NodePath>,
4954}
4955
4956impl<T> From<&ast::Node<T>> for AstNodeRef {
4957    fn from(value: &ast::Node<T>) -> Self {
4958        AstNodeRef {
4959            range: value.into(),
4960            node_path: value.node_path.clone(),
4961        }
4962    }
4963}
4964
4965impl From<&ast::BodyItem> for AstNodeRef {
4966    fn from(value: &ast::BodyItem) -> Self {
4967        match value {
4968            ast::BodyItem::ImportStatement(node) => AstNodeRef {
4969                range: node.into(),
4970                node_path: node.node_path.clone(),
4971            },
4972            ast::BodyItem::ExpressionStatement(node) => AstNodeRef {
4973                range: node.into(),
4974                node_path: node.node_path.clone(),
4975            },
4976            ast::BodyItem::VariableDeclaration(node) => AstNodeRef {
4977                range: node.into(),
4978                node_path: node.node_path.clone(),
4979            },
4980            ast::BodyItem::TypeDeclaration(node) => AstNodeRef {
4981                range: node.into(),
4982                node_path: node.node_path.clone(),
4983            },
4984            ast::BodyItem::ReturnStatement(node) => AstNodeRef {
4985                range: node.into(),
4986                node_path: node.node_path.clone(),
4987            },
4988        }
4989    }
4990}
4991
4992impl From<&ast::Expr> for AstNodeRef {
4993    fn from(value: &ast::Expr) -> Self {
4994        AstNodeRef {
4995            range: SourceRange::from(value),
4996            node_path: value.node_path().cloned(),
4997        }
4998    }
4999}
5000
5001impl From<&AstMutateContext> for AstNodeRef {
5002    fn from(value: &AstMutateContext) -> Self {
5003        AstNodeRef {
5004            range: value.source_range,
5005            node_path: value.node_path.clone(),
5006        }
5007    }
5008}
5009
5010impl TryFrom<&NodeMut<'_>> for AstNodeRef {
5011    type Error = crate::walk::AstNodeError;
5012
5013    fn try_from(value: &NodeMut<'_>) -> Result<Self, Self::Error> {
5014        Ok(AstNodeRef {
5015            range: SourceRange::try_from(value)?,
5016            node_path: value.try_into()?,
5017        })
5018    }
5019}
5020
5021impl From<AstNodeRef> for SourceRange {
5022    fn from(value: AstNodeRef) -> Self {
5023        value.range
5024    }
5025}
5026
5027impl Visitor for AstMutateContext {
5028    type Break = Result<(AstNodeRef, AstMutateCommandReturn), KclError>;
5029    type Continue = ();
5030
5031    fn visit(&mut self, node: NodeMut<'_>) -> TraversalReturn<Self::Break, Self::Continue> {
5032        filter_and_process(self, node)
5033    }
5034
5035    fn finish(&mut self, node: NodeMut<'_>) {
5036        match &node {
5037            NodeMut::Program(_) | NodeMut::SketchBlock(_) => {
5038                self.defined_names_stack.pop();
5039            }
5040            _ => {}
5041        }
5042    }
5043}
5044
5045fn filter_and_process(
5046    ctx: &mut AstMutateContext,
5047    node: NodeMut,
5048) -> TraversalReturn<Result<(AstNodeRef, AstMutateCommandReturn), KclError>> {
5049    let Ok(node_range) = SourceRange::try_from(&node) else {
5050        // Nodes that can't be converted to a range aren't interesting.
5051        return TraversalReturn::new_continue(());
5052    };
5053    // If we're adding a variable declaration, we need to look at variable
5054    // declaration expressions to see if it already has a variable, before
5055    // continuing. The variable declaration's source range won't match the
5056    // target; its init expression will.
5057    if let NodeMut::VariableDeclaration(var_decl) = &node {
5058        let expr_range = SourceRange::from(&var_decl.declaration.init);
5059        let expr_node_path = var_decl.declaration.init.node_path();
5060        if source_ref_matches(ctx, expr_range, expr_node_path) {
5061            if let AstMutateCommand::AddVariableDeclaration { .. } = &ctx.command {
5062                // We found the variable declaration expression. It doesn't need
5063                // to be added.
5064                return TraversalReturn::new_break(Ok((
5065                    AstNodeRef::from(&**var_decl),
5066                    AstMutateCommandReturn::Name(var_decl.name().to_owned()),
5067                )));
5068            }
5069            if let AstMutateCommand::DeleteNode = &ctx.command {
5070                // We found the variable declaration. Delete the variable along
5071                // with the segment.
5072                return TraversalReturn {
5073                    mutate_body_item: MutateBodyItem::Delete,
5074                    control_flow: ControlFlow::Break(Ok((AstNodeRef::from(&*ctx), AstMutateCommandReturn::None))),
5075                };
5076            }
5077        }
5078    }
5079    // Similar thing with expression statement. We need to look at the
5080    // expression inside it.
5081    if let NodeMut::ExpressionStatement(expr_stmt) = &node {
5082        let expr_range = SourceRange::from(&expr_stmt.expression);
5083        let expr_node_path = expr_stmt.expression.node_path();
5084        if source_ref_matches(ctx, expr_range, expr_node_path) {
5085            if let AstMutateCommand::AddVariableDeclaration { .. } = &ctx.command {
5086                // We found the node wrapped in an expression statement. Process
5087                // the statement.
5088                let Ok(node_ref) = AstNodeRef::try_from(&node) else {
5089                    return TraversalReturn::new_continue(());
5090                };
5091                return process(ctx, node).map_break(|result| result.map(|cmd_return| (node_ref, cmd_return)));
5092            }
5093            if let AstMutateCommand::DeleteNode = &ctx.command {
5094                // We found the node wrapped in an expression statement. Delete
5095                // the whole statement.
5096                return TraversalReturn {
5097                    mutate_body_item: MutateBodyItem::Delete,
5098                    control_flow: ControlFlow::Break(Ok((AstNodeRef::from(&*ctx), AstMutateCommandReturn::None))),
5099                };
5100            }
5101        }
5102    }
5103
5104    if ctx.command.needs_defined_names_stack() {
5105        if let NodeMut::Program(program) = &node {
5106            ctx.defined_names_stack.push(find_defined_names(*program));
5107        } else if let NodeMut::SketchBlock(block) = &node {
5108            ctx.defined_names_stack.push(find_defined_names(&block.body));
5109        }
5110    }
5111
5112    // Make sure the node matches the source ref.
5113    let node_path = <Option<ast::NodePath>>::try_from(&node).ok().flatten();
5114    if !source_ref_matches(ctx, node_range, node_path.as_ref()) {
5115        return TraversalReturn::new_continue(());
5116    }
5117    let Ok(node_ref) = AstNodeRef::try_from(&node) else {
5118        return TraversalReturn::new_continue(());
5119    };
5120    process(ctx, node).map_break(|result| result.map(|cmd_return| (node_ref, cmd_return)))
5121}
5122
5123fn source_ref_matches(ctx: &AstMutateContext, node_range: SourceRange, node_path: Option<&ast::NodePath>) -> bool {
5124    match &ctx.node_path {
5125        Some(target) => Some(target) == node_path,
5126        None => node_range == ctx.source_range,
5127    }
5128}
5129
5130fn process(ctx: &AstMutateContext, node: NodeMut) -> TraversalReturn<Result<AstMutateCommandReturn, KclError>> {
5131    match &ctx.command {
5132        AstMutateCommand::AddSketchBlockExprStmt { expr } => {
5133            if let NodeMut::SketchBlock(sketch_block) = node {
5134                sketch_block
5135                    .body
5136                    .items
5137                    .push(ast::BodyItem::ExpressionStatement(ast::Node {
5138                        inner: ast::ExpressionStatement {
5139                            expression: expr.clone(),
5140                            digest: None,
5141                        },
5142                        start: Default::default(),
5143                        end: Default::default(),
5144                        module_id: Default::default(),
5145                        node_path: None,
5146                        outer_attrs: Default::default(),
5147                        pre_comments: Default::default(),
5148                        comment_start: Default::default(),
5149                    }));
5150                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5151            }
5152        }
5153        AstMutateCommand::AddSketchBlockVarDecl { prefix, expr } => {
5154            if let NodeMut::SketchBlock(sketch_block) = node {
5155                let empty_defined_names = HashSet::new();
5156                let defined_names = ctx.defined_names_stack.last().unwrap_or(&empty_defined_names);
5157                let Ok(name) = next_free_name(prefix, defined_names) else {
5158                    return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5159                };
5160                sketch_block
5161                    .body
5162                    .items
5163                    .push(ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(
5164                        ast::VariableDeclaration::new(
5165                            ast::VariableDeclarator::new(&name, expr.clone()),
5166                            ast::ItemVisibility::Default,
5167                            ast::VariableKind::Const,
5168                        ),
5169                    ))));
5170                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::Name(name)));
5171            }
5172        }
5173        AstMutateCommand::AddVariableDeclaration { prefix } => {
5174            if let NodeMut::VariableDeclaration(inner) = node {
5175                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::Name(inner.name().to_owned())));
5176            }
5177            if let NodeMut::ExpressionStatement(expr_stmt) = node {
5178                let empty_defined_names = HashSet::new();
5179                let defined_names = ctx.defined_names_stack.last().unwrap_or(&empty_defined_names);
5180                let Ok(name) = next_free_name(prefix, defined_names) else {
5181                    // TODO: Return an error instead?
5182                    return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5183                };
5184                let mutate_node =
5185                    ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(ast::VariableDeclaration::new(
5186                        ast::VariableDeclarator::new(&name, expr_stmt.expression.clone()),
5187                        ast::ItemVisibility::Default,
5188                        ast::VariableKind::Const,
5189                    ))));
5190                return TraversalReturn {
5191                    mutate_body_item: MutateBodyItem::Mutate(Box::new(mutate_node)),
5192                    control_flow: ControlFlow::Break(Ok(AstMutateCommandReturn::Name(name))),
5193                };
5194            }
5195        }
5196        AstMutateCommand::EditPoint { at } => {
5197            if let NodeMut::CallExpressionKw(call) = node {
5198                if call.callee.name.name != POINT_FN {
5199                    return TraversalReturn::new_continue(());
5200                }
5201                // Update the arguments.
5202                for labeled_arg in &mut call.arguments {
5203                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(POINT_AT_PARAM) {
5204                        labeled_arg.arg = at.clone();
5205                    }
5206                }
5207                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5208            }
5209        }
5210        AstMutateCommand::EditLine {
5211            start,
5212            end,
5213            construction,
5214        } => {
5215            if let NodeMut::CallExpressionKw(call) = node {
5216                if call.callee.name.name != LINE_FN {
5217                    return TraversalReturn::new_continue(());
5218                }
5219                // Update the arguments.
5220                for labeled_arg in &mut call.arguments {
5221                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(LINE_START_PARAM) {
5222                        labeled_arg.arg = start.clone();
5223                    }
5224                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(LINE_END_PARAM) {
5225                        labeled_arg.arg = end.clone();
5226                    }
5227                }
5228                // Handle construction kwarg
5229                if let Some(construction_value) = construction {
5230                    let construction_exists = call
5231                        .arguments
5232                        .iter()
5233                        .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
5234                    if *construction_value {
5235                        // Add or update construction=true
5236                        if construction_exists {
5237                            // Update existing construction kwarg
5238                            for labeled_arg in &mut call.arguments {
5239                                if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
5240                                    labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
5241                                        value: ast::LiteralValue::Bool(true),
5242                                        raw: "true".to_string(),
5243                                        digest: None,
5244                                    })));
5245                                }
5246                            }
5247                        } else {
5248                            // Add new construction kwarg
5249                            call.arguments.push(ast::LabeledArg {
5250                                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
5251                                arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
5252                                    value: ast::LiteralValue::Bool(true),
5253                                    raw: "true".to_string(),
5254                                    digest: None,
5255                                }))),
5256                            });
5257                        }
5258                    } else {
5259                        // Remove construction kwarg if it exists
5260                        call.arguments
5261                            .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
5262                    }
5263                }
5264                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5265            }
5266        }
5267        AstMutateCommand::EditArc {
5268            start,
5269            end,
5270            center,
5271            construction,
5272        } => {
5273            if let NodeMut::CallExpressionKw(call) = node {
5274                if call.callee.name.name != ARC_FN {
5275                    return TraversalReturn::new_continue(());
5276                }
5277                // Update the arguments.
5278                for labeled_arg in &mut call.arguments {
5279                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_START_PARAM) {
5280                        labeled_arg.arg = start.clone();
5281                    }
5282                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_END_PARAM) {
5283                        labeled_arg.arg = end.clone();
5284                    }
5285                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_CENTER_PARAM) {
5286                        labeled_arg.arg = center.clone();
5287                    }
5288                }
5289                // Handle construction kwarg
5290                if let Some(construction_value) = construction {
5291                    let construction_exists = call
5292                        .arguments
5293                        .iter()
5294                        .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
5295                    if *construction_value {
5296                        // Add or update construction=true
5297                        if construction_exists {
5298                            // Update existing construction kwarg
5299                            for labeled_arg in &mut call.arguments {
5300                                if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
5301                                    labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
5302                                        value: ast::LiteralValue::Bool(true),
5303                                        raw: "true".to_string(),
5304                                        digest: None,
5305                                    })));
5306                                }
5307                            }
5308                        } else {
5309                            // Add new construction kwarg
5310                            call.arguments.push(ast::LabeledArg {
5311                                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
5312                                arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
5313                                    value: ast::LiteralValue::Bool(true),
5314                                    raw: "true".to_string(),
5315                                    digest: None,
5316                                }))),
5317                            });
5318                        }
5319                    } else {
5320                        // Remove construction kwarg if it exists
5321                        call.arguments
5322                            .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
5323                    }
5324                }
5325                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5326            }
5327        }
5328        AstMutateCommand::EditCircle {
5329            start,
5330            center,
5331            construction,
5332        } => {
5333            if let NodeMut::CallExpressionKw(call) = node {
5334                if call.callee.name.name != CIRCLE_FN {
5335                    return TraversalReturn::new_continue(());
5336                }
5337                // Update the arguments.
5338                for labeled_arg in &mut call.arguments {
5339                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CIRCLE_START_PARAM) {
5340                        labeled_arg.arg = start.clone();
5341                    }
5342                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CIRCLE_CENTER_PARAM) {
5343                        labeled_arg.arg = center.clone();
5344                    }
5345                }
5346                // Handle construction kwarg
5347                if let Some(construction_value) = construction {
5348                    let construction_exists = call
5349                        .arguments
5350                        .iter()
5351                        .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
5352                    if *construction_value {
5353                        if construction_exists {
5354                            for labeled_arg in &mut call.arguments {
5355                                if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
5356                                    labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
5357                                        value: ast::LiteralValue::Bool(true),
5358                                        raw: "true".to_string(),
5359                                        digest: None,
5360                                    })));
5361                                }
5362                            }
5363                        } else {
5364                            call.arguments.push(ast::LabeledArg {
5365                                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
5366                                arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
5367                                    value: ast::LiteralValue::Bool(true),
5368                                    raw: "true".to_string(),
5369                                    digest: None,
5370                                }))),
5371                            });
5372                        }
5373                    } else {
5374                        call.arguments
5375                            .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
5376                    }
5377                }
5378                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5379            }
5380        }
5381        AstMutateCommand::EditConstraintValue { value } => {
5382            if let NodeMut::BinaryExpression(binary_expr) = node {
5383                let left_is_constraint = matches!(
5384                    &binary_expr.left,
5385                    ast::BinaryPart::CallExpressionKw(call)
5386                        if matches!(
5387                            call.callee.name.name.as_str(),
5388                            DISTANCE_FN | HORIZONTAL_DISTANCE_FN | VERTICAL_DISTANCE_FN | RADIUS_FN | DIAMETER_FN | ANGLE_FN
5389                        )
5390                );
5391                if left_is_constraint {
5392                    binary_expr.right = value.clone();
5393                } else {
5394                    binary_expr.left = value.clone();
5395                }
5396
5397                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5398            }
5399        }
5400        AstMutateCommand::EditDistanceConstraintLabelPosition { label_position } => {
5401            if let NodeMut::BinaryExpression(binary_expr) = node {
5402                let ast::BinaryPart::CallExpressionKw(call) = &mut binary_expr.left else {
5403                    return TraversalReturn::new_continue(());
5404                };
5405                if !matches!(
5406                    call.callee.name.name.as_str(),
5407                    DISTANCE_FN | HORIZONTAL_DISTANCE_FN | VERTICAL_DISTANCE_FN
5408                ) {
5409                    return TraversalReturn::new_continue(());
5410                }
5411
5412                if let Some(label_arg) = call
5413                    .arguments
5414                    .iter_mut()
5415                    .find(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(LABEL_POSITION_PARAM))
5416                {
5417                    label_arg.arg = label_position.clone();
5418                } else {
5419                    call.arguments.push(ast::LabeledArg {
5420                        label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
5421                        arg: label_position.clone(),
5422                    });
5423                }
5424
5425                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5426            }
5427        }
5428        AstMutateCommand::EditCallUnlabeled { arg } => {
5429            if let NodeMut::CallExpressionKw(call) = node {
5430                call.unlabeled = Some(arg.clone());
5431                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5432            }
5433        }
5434        #[cfg(feature = "artifact-graph")]
5435        AstMutateCommand::EditVarInitialValue { value } => {
5436            if let NodeMut::NumericLiteral(numeric_literal) = node {
5437                // Update the initial value.
5438                let Ok(literal) = to_source_number(*value) else {
5439                    return TraversalReturn::new_break(Err(KclError::refactor(format!(
5440                        "Could not convert number to AST literal: {:?}",
5441                        *value
5442                    ))));
5443                };
5444                *numeric_literal = ast::Node::no_src(literal);
5445                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5446            }
5447        }
5448        AstMutateCommand::DeleteNode => {
5449            return TraversalReturn {
5450                mutate_body_item: MutateBodyItem::Delete,
5451                control_flow: ControlFlow::Break(Ok(AstMutateCommandReturn::None)),
5452            };
5453        }
5454    }
5455    TraversalReturn::new_continue(())
5456}
5457
5458struct FindSketchBlockSourceRange {
5459    /// The source range of the sketch block before mutation.
5460    target_before_mutation: SourceRange,
5461    /// The source range of the sketch block's last body item after mutation. We
5462    /// need to use a [Cell] since the [crate::walk::Visitor] trait requires a
5463    /// shared reference.
5464    found: Cell<Option<AstNodeRef>>,
5465}
5466
5467impl<'a> crate::walk::Visitor<'a> for &FindSketchBlockSourceRange {
5468    type Error = crate::front::Error;
5469
5470    fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
5471        let Ok(node_range) = SourceRange::try_from(&node) else {
5472            return Ok(true);
5473        };
5474
5475        if let crate::walk::Node::SketchBlock(sketch_block) = node {
5476            if node_range.module_id() == self.target_before_mutation.module_id()
5477                && node_range.start() == self.target_before_mutation.start()
5478                // End shouldn't match since we added something.
5479                && node_range.end() >= self.target_before_mutation.end()
5480            {
5481                self.found.set(sketch_block.body.items.last().map(|item| match item {
5482                    // For declarations like `circle1 = circle(...)`, use
5483                    // the init expression range so lookup in source_range_to_object
5484                    // matches the segment source range.
5485                    ast::BodyItem::VariableDeclaration(node) => AstNodeRef::from(&node.declaration.init),
5486                    _ => AstNodeRef::from(item),
5487                }));
5488                return Ok(false);
5489            } else {
5490                // We found a different sketch block. No need to descend into
5491                // its children since sketch blocks cannot be nested.
5492                return Ok(true);
5493            }
5494        }
5495
5496        for child in node.children().iter() {
5497            if !child.visit(*self)? {
5498                return Ok(false);
5499            }
5500        }
5501
5502        Ok(true)
5503    }
5504}
5505
5506struct FindSketchBlockByNodePath {
5507    /// The Node Path of the sketch block before mutation.
5508    target_node_path: ast::NodePath,
5509    /// The ref of the sketch block's last body item after mutation. We need to
5510    /// use a [Cell] since the [crate::walk::Visitor] trait requires a shared
5511    /// reference.
5512    found: Cell<Option<AstNodeRef>>,
5513}
5514
5515impl<'a> crate::walk::Visitor<'a> for &FindSketchBlockByNodePath {
5516    type Error = crate::front::Error;
5517
5518    fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
5519        let Ok(node_path) = <Option<ast::NodePath>>::try_from(&node) else {
5520            return Ok(true);
5521        };
5522
5523        if let crate::walk::Node::SketchBlock(sketch_block) = node {
5524            if let Some(node_path) = node_path
5525                && node_path == self.target_node_path
5526            {
5527                self.found.set(sketch_block.body.items.last().map(|item| match item {
5528                    // For declarations like `circle1 = circle(...)`, use
5529                    // the init expression range so lookup in source_range_to_object
5530                    // matches the segment source range.
5531                    ast::BodyItem::VariableDeclaration(node) => AstNodeRef::from(&node.declaration.init),
5532                    _ => AstNodeRef::from(item),
5533                }));
5534
5535                return Ok(false);
5536            } else {
5537                // We found a different sketch block. No need to descend into
5538                // its children since sketch blocks cannot be nested.
5539                return Ok(true);
5540            }
5541        }
5542
5543        for child in node.children().iter() {
5544            if !child.visit(*self)? {
5545                return Ok(false);
5546            }
5547        }
5548
5549        Ok(true)
5550    }
5551}
5552
5553/// After adding an item to a sketch block, find the sketch block, and get the
5554/// source range of the added item. We assume that the added item is the last
5555/// item in the sketch block and that the sketch block's source range has grown,
5556/// but not moved from its starting offset.
5557///
5558/// TODO: Do we need to format *before* mutation in case formatting moves the
5559/// sketch block forward?
5560fn find_sketch_block_added_item(
5561    ast: &ast::Node<ast::Program>,
5562    sketch_block_before_mutation: &AstNodeRef,
5563) -> Result<AstNodeRef, KclError> {
5564    if let Some(node_path) = &sketch_block_before_mutation.node_path {
5565        let find = FindSketchBlockByNodePath {
5566            target_node_path: node_path.clone(),
5567            found: Cell::new(None),
5568        };
5569        let node = crate::walk::Node::from(ast);
5570        node.visit(&find).map_err(|err| KclError::refactor(err.msg))?;
5571        find.found.into_inner().ok_or_else(|| {
5572            KclError::refactor(format!(
5573                "Node ID after mutation not found for Node ID before mutation: {node_path:?}"
5574            ))
5575        })
5576    } else {
5577        // No NodePath. Fall back to legacy source range.
5578        let find = FindSketchBlockSourceRange {
5579            target_before_mutation: sketch_block_before_mutation.range,
5580            found: Cell::new(None),
5581        };
5582        let node = crate::walk::Node::from(ast);
5583        node.visit(&find).map_err(|err| KclError::refactor(err.msg))?;
5584        find.found.into_inner().ok_or_else(|| KclError::refactor(
5585            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?"),
5586        ))
5587    }
5588}
5589
5590fn source_from_ast(ast: &ast::Node<ast::Program>) -> String {
5591    // TODO: Don't duplicate this from lib.rs Program.
5592    ast.recast_top(&Default::default(), 0)
5593}
5594
5595pub(crate) fn to_ast_point2d(point: &Point2d<Expr>) -> anyhow::Result<ast::Expr> {
5596    Ok(ast::Expr::ArrayExpression(Box::new(ast::Node {
5597        inner: ast::ArrayExpression {
5598            elements: vec![to_source_expr(&point.x)?, to_source_expr(&point.y)?],
5599            non_code_meta: Default::default(),
5600            digest: None,
5601        },
5602        start: Default::default(),
5603        end: Default::default(),
5604        module_id: Default::default(),
5605        node_path: None,
5606        outer_attrs: Default::default(),
5607        pre_comments: Default::default(),
5608        comment_start: Default::default(),
5609    })))
5610}
5611
5612fn to_ast_point2d_number(point: &Point2d<Number>) -> anyhow::Result<ast::Expr> {
5613    Ok(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
5614        ast::ArrayExpression {
5615            elements: vec![
5616                ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(to_source_number(
5617                    point.x,
5618                )?)))),
5619                ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(to_source_number(
5620                    point.y,
5621                )?)))),
5622            ],
5623            non_code_meta: Default::default(),
5624            digest: None,
5625        },
5626    ))))
5627}
5628
5629fn to_source_expr(expr: &Expr) -> anyhow::Result<ast::Expr> {
5630    match expr {
5631        Expr::Number(number) => Ok(ast::Expr::Literal(Box::new(ast::Node {
5632            inner: ast::Literal::from(to_source_number(*number)?),
5633            start: Default::default(),
5634            end: Default::default(),
5635            module_id: Default::default(),
5636            node_path: None,
5637            outer_attrs: Default::default(),
5638            pre_comments: Default::default(),
5639            comment_start: Default::default(),
5640        }))),
5641        Expr::Var(number) => Ok(ast::Expr::SketchVar(Box::new(ast::Node {
5642            inner: ast::SketchVar {
5643                initial: Some(Box::new(ast::Node {
5644                    inner: to_source_number(*number)?,
5645                    start: Default::default(),
5646                    end: Default::default(),
5647                    module_id: Default::default(),
5648                    node_path: None,
5649                    outer_attrs: Default::default(),
5650                    pre_comments: Default::default(),
5651                    comment_start: Default::default(),
5652                })),
5653                digest: None,
5654            },
5655            start: Default::default(),
5656            end: Default::default(),
5657            module_id: Default::default(),
5658            node_path: None,
5659            outer_attrs: Default::default(),
5660            pre_comments: Default::default(),
5661            comment_start: Default::default(),
5662        }))),
5663        Expr::Variable(variable) => Ok(ast_name_expr(variable.clone())),
5664    }
5665}
5666
5667fn to_source_number(number: Number) -> anyhow::Result<ast::NumericLiteral> {
5668    Ok(ast::NumericLiteral {
5669        value: number.value,
5670        suffix: number.units,
5671        raw: format_number_literal(number.value, number.units, None)?,
5672        digest: None,
5673    })
5674}
5675
5676pub(crate) fn ast_name_expr(name: String) -> ast::Expr {
5677    ast::Expr::Name(Box::new(ast_name(name)))
5678}
5679
5680fn ast_name(name: String) -> ast::Node<ast::Name> {
5681    ast::Node {
5682        inner: ast::Name {
5683            name: ast::Node {
5684                inner: ast::Identifier { name, digest: None },
5685                start: Default::default(),
5686                end: Default::default(),
5687                module_id: Default::default(),
5688                node_path: None,
5689                outer_attrs: Default::default(),
5690                pre_comments: Default::default(),
5691                comment_start: Default::default(),
5692            },
5693            path: Vec::new(),
5694            abs_path: false,
5695            digest: None,
5696        },
5697        start: Default::default(),
5698        end: Default::default(),
5699        module_id: Default::default(),
5700        node_path: None,
5701        outer_attrs: Default::default(),
5702        pre_comments: Default::default(),
5703        comment_start: Default::default(),
5704    }
5705}
5706
5707pub(crate) fn ast_sketch2_name(name: &str) -> ast::Name {
5708    ast::Name {
5709        name: ast::Node {
5710            inner: ast::Identifier {
5711                name: name.to_owned(),
5712                digest: None,
5713            },
5714            start: Default::default(),
5715            end: Default::default(),
5716            module_id: Default::default(),
5717            node_path: None,
5718            outer_attrs: Default::default(),
5719            pre_comments: Default::default(),
5720            comment_start: Default::default(),
5721        },
5722        path: Default::default(),
5723        abs_path: false,
5724        digest: None,
5725    }
5726}
5727
5728// Shared AST creation helpers used by both frontend and transpiler to ensure consistency.
5729
5730/// Create an AST node for coincident([expr1, expr2, ...])
5731pub(crate) fn create_coincident_ast(exprs: impl IntoIterator<Item = ast::Expr>) -> ast::Expr {
5732    let elements = exprs.into_iter().collect::<Vec<_>>();
5733    debug_assert!(elements.len() >= 2, "Coincident AST should have at least 2 inputs");
5734
5735    // Create array [expr1, expr2, ...]
5736    let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
5737        elements,
5738        digest: None,
5739        non_code_meta: Default::default(),
5740    })));
5741
5742    // Create coincident([...])
5743    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5744        callee: ast::Node::no_src(ast_sketch2_name(COINCIDENT_FN)),
5745        unlabeled: Some(array_expr),
5746        arguments: Default::default(),
5747        digest: None,
5748        non_code_meta: Default::default(),
5749    })))
5750}
5751
5752/// Create an AST node for line(start = [...], end = [...])
5753pub(crate) fn create_line_ast(start_ast: ast::Expr, end_ast: ast::Expr) -> ast::Expr {
5754    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5755        callee: ast::Node::no_src(ast_sketch2_name(LINE_FN)),
5756        unlabeled: None,
5757        arguments: vec![
5758            ast::LabeledArg {
5759                label: Some(ast::Identifier::new(LINE_START_PARAM)),
5760                arg: start_ast,
5761            },
5762            ast::LabeledArg {
5763                label: Some(ast::Identifier::new(LINE_END_PARAM)),
5764                arg: end_ast,
5765            },
5766        ],
5767        digest: None,
5768        non_code_meta: Default::default(),
5769    })))
5770}
5771
5772/// Create an AST node for arc(start = [...], end = [...], center = [...])
5773pub(crate) fn create_arc_ast(start_ast: ast::Expr, end_ast: ast::Expr, center_ast: ast::Expr) -> ast::Expr {
5774    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5775        callee: ast::Node::no_src(ast_sketch2_name(ARC_FN)),
5776        unlabeled: None,
5777        arguments: vec![
5778            ast::LabeledArg {
5779                label: Some(ast::Identifier::new(ARC_START_PARAM)),
5780                arg: start_ast,
5781            },
5782            ast::LabeledArg {
5783                label: Some(ast::Identifier::new(ARC_END_PARAM)),
5784                arg: end_ast,
5785            },
5786            ast::LabeledArg {
5787                label: Some(ast::Identifier::new(ARC_CENTER_PARAM)),
5788                arg: center_ast,
5789            },
5790        ],
5791        digest: None,
5792        non_code_meta: Default::default(),
5793    })))
5794}
5795
5796/// Create an AST node for circle(start = [...], center = [...])
5797pub(crate) fn create_circle_ast(start_ast: ast::Expr, center_ast: ast::Expr) -> ast::Expr {
5798    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5799        callee: ast::Node::no_src(ast_sketch2_name(CIRCLE_FN)),
5800        unlabeled: None,
5801        arguments: vec![
5802            ast::LabeledArg {
5803                label: Some(ast::Identifier::new(CIRCLE_START_PARAM)),
5804                arg: start_ast,
5805            },
5806            ast::LabeledArg {
5807                label: Some(ast::Identifier::new(CIRCLE_CENTER_PARAM)),
5808                arg: center_ast,
5809            },
5810        ],
5811        digest: None,
5812        non_code_meta: Default::default(),
5813    })))
5814}
5815
5816/// Create an AST node for horizontal(line)
5817pub(crate) fn create_horizontal_ast(line_expr: ast::Expr) -> ast::Expr {
5818    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5819        callee: ast::Node::no_src(ast_sketch2_name(HORIZONTAL_FN)),
5820        unlabeled: Some(line_expr),
5821        arguments: Default::default(),
5822        digest: None,
5823        non_code_meta: Default::default(),
5824    })))
5825}
5826
5827/// Create an AST node for vertical(line)
5828pub(crate) fn create_vertical_ast(line_expr: ast::Expr) -> ast::Expr {
5829    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5830        callee: ast::Node::no_src(ast_sketch2_name(VERTICAL_FN)),
5831        unlabeled: Some(line_expr),
5832        arguments: Default::default(),
5833        digest: None,
5834        non_code_meta: Default::default(),
5835    })))
5836}
5837
5838/// Create a member expression like object.property (e.g., line1.end)
5839pub(crate) fn create_member_expression(object_expr: ast::Expr, property: &str) -> ast::Expr {
5840    ast::Expr::MemberExpression(Box::new(ast::Node::no_src(ast::MemberExpression {
5841        object: object_expr,
5842        property: ast::Expr::Name(Box::new(ast::Node::no_src(ast::Name {
5843            name: ast::Node::no_src(ast::Identifier {
5844                name: property.to_string(),
5845                digest: None,
5846            }),
5847            path: Vec::new(),
5848            abs_path: false,
5849            digest: None,
5850        }))),
5851        computed: false,
5852        digest: None,
5853    })))
5854}
5855
5856/// Create an AST node for `fixed([point, [x, y]])`.
5857fn create_fixed_point_constraint_ast(point_expr: ast::Expr, position: Point2d<Number>) -> anyhow::Result<ast::Expr> {
5858    // Create [x, y] array literal.
5859    let x_literal = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(to_source_number(
5860        position.x,
5861    )?))));
5862    let y_literal = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(to_source_number(
5863        position.y,
5864    )?))));
5865    let point_array = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
5866        elements: vec![x_literal, y_literal],
5867        digest: None,
5868        non_code_meta: Default::default(),
5869    })));
5870
5871    // Create [point, [x, y]] outer array.
5872    let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
5873        elements: vec![point_expr, point_array],
5874        digest: None,
5875        non_code_meta: Default::default(),
5876    })));
5877
5878    // Create fixed([...])
5879    Ok(ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(
5880        ast::CallExpressionKw {
5881            callee: ast::Node::no_src(ast_sketch2_name(FIXED_FN)),
5882            unlabeled: Some(array_expr),
5883            arguments: Default::default(),
5884            digest: None,
5885            non_code_meta: Default::default(),
5886        },
5887    ))))
5888}
5889
5890/// Create an AST node for equalLength([line1, line2, ...])
5891pub(crate) fn create_equal_length_ast(line_exprs: Vec<ast::Expr>) -> ast::Expr {
5892    let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
5893        elements: line_exprs,
5894        digest: None,
5895        non_code_meta: Default::default(),
5896    })));
5897
5898    // Create equalLength([...])
5899    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5900        callee: ast::Node::no_src(ast_sketch2_name(EQUAL_LENGTH_FN)),
5901        unlabeled: Some(array_expr),
5902        arguments: Default::default(),
5903        digest: None,
5904        non_code_meta: Default::default(),
5905    })))
5906}
5907
5908/// Create an AST node for equalRadius([seg1, seg2, ...])
5909pub(crate) fn create_equal_radius_ast(segment_exprs: Vec<ast::Expr>) -> ast::Expr {
5910    let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
5911        elements: segment_exprs,
5912        digest: None,
5913        non_code_meta: Default::default(),
5914    })));
5915
5916    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5917        callee: ast::Node::no_src(ast_sketch2_name(EQUAL_RADIUS_FN)),
5918        unlabeled: Some(array_expr),
5919        arguments: Default::default(),
5920        digest: None,
5921        non_code_meta: Default::default(),
5922    })))
5923}
5924
5925/// Create an AST node for tangent([seg1, seg2])
5926pub(crate) fn create_tangent_ast(seg1_expr: ast::Expr, seg2_expr: ast::Expr) -> ast::Expr {
5927    let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
5928        elements: vec![seg1_expr, seg2_expr],
5929        digest: None,
5930        non_code_meta: Default::default(),
5931    })));
5932
5933    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5934        callee: ast::Node::no_src(ast_sketch2_name(TANGENT_FN)),
5935        unlabeled: Some(array_expr),
5936        arguments: Default::default(),
5937        digest: None,
5938        non_code_meta: Default::default(),
5939    })))
5940}
5941
5942/// Create an AST node for symmetric([input1, input2], axis = line)
5943pub(crate) fn create_symmetric_ast(input_exprs: Vec<ast::Expr>, axis_expr: ast::Expr) -> ast::Expr {
5944    let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
5945        elements: input_exprs,
5946        digest: None,
5947        non_code_meta: Default::default(),
5948    })));
5949    let arguments = vec![ast::LabeledArg {
5950        label: Some(ast::Identifier::new(SYMMETRIC_AXIS_PARAM)),
5951        arg: axis_expr,
5952    }];
5953
5954    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5955        callee: ast::Node::no_src(ast_sketch2_name(SYMMETRIC_FN)),
5956        unlabeled: Some(array_expr),
5957        arguments,
5958        digest: None,
5959        non_code_meta: Default::default(),
5960    })))
5961}
5962
5963/// Create an AST node for midpoint(segment, point = point)
5964pub(crate) fn create_midpoint_ast(segment_expr: ast::Expr, point_expr: ast::Expr) -> ast::Expr {
5965    let arguments = vec![ast::LabeledArg {
5966        label: Some(ast::Identifier::new(MIDPOINT_POINT_PARAM)),
5967        arg: point_expr,
5968    }];
5969
5970    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5971        callee: ast::Node::no_src(ast_sketch2_name(MIDPOINT_FN)),
5972        unlabeled: Some(segment_expr),
5973        arguments,
5974        digest: None,
5975        non_code_meta: Default::default(),
5976    })))
5977}
5978
5979#[cfg(all(feature = "artifact-graph", test))]
5980mod tests {
5981    use super::*;
5982    use crate::engine::PlaneName;
5983    use crate::execution::cache::SketchModeState;
5984    use crate::execution::cache::clear_mem_cache;
5985    use crate::execution::cache::read_old_memory;
5986    use crate::execution::cache::write_old_memory;
5987    use crate::front::Distance;
5988    use crate::front::Fixed;
5989    use crate::front::FixedPoint;
5990    use crate::front::Midpoint;
5991    use crate::front::Object;
5992    use crate::front::Plane;
5993    use crate::front::Sketch;
5994    use crate::front::Tangent;
5995    use crate::frontend::sketch::Vertical;
5996    use crate::pretty::NumericSuffix;
5997
5998    fn find_first_sketch_object(scene_graph: &SceneGraph) -> Option<&Object> {
5999        for object in &scene_graph.objects {
6000            if let ObjectKind::Sketch(_) = &object.kind {
6001                return Some(object);
6002            }
6003        }
6004        None
6005    }
6006
6007    fn find_first_face_object(scene_graph: &SceneGraph) -> Option<&Object> {
6008        for object in &scene_graph.objects {
6009            if let ObjectKind::Face(_) = &object.kind {
6010                return Some(object);
6011            }
6012        }
6013        None
6014    }
6015
6016    fn find_first_wall_object_id(scene_graph: &SceneGraph) -> Option<ObjectId> {
6017        for object in &scene_graph.objects {
6018            if matches!(&object.kind, ObjectKind::Wall(_)) {
6019                return Some(object.id);
6020            }
6021        }
6022        None
6023    }
6024
6025    #[test]
6026    fn test_region_name_from_sweep_variable_supports_sweep_kinds() {
6027        let source = "\
6028region001 = region(point = [0.1, 0.1], sketch = s)
6029extrude001 = extrude(region001, length = 5)
6030revolve001 = revolve(region001, axis = Y)
6031sweep001 = sweep(region001, path = path001)
6032loft001 = loft(region001)
6033not_sweep001 = shell(extrude001, faces = [], thickness = 1)
6034";
6035
6036        let program = Program::parse(source).unwrap().0.unwrap();
6037
6038        assert_eq!(
6039            region_name_from_sweep_variable(&program.ast, "extrude001"),
6040            Some("region001".to_owned())
6041        );
6042        assert_eq!(
6043            region_name_from_sweep_variable(&program.ast, "revolve001"),
6044            Some("region001".to_owned())
6045        );
6046        assert_eq!(
6047            region_name_from_sweep_variable(&program.ast, "sweep001"),
6048            Some("region001".to_owned())
6049        );
6050        assert_eq!(
6051            region_name_from_sweep_variable(&program.ast, "loft001"),
6052            Some("region001".to_owned())
6053        );
6054        assert_eq!(region_name_from_sweep_variable(&program.ast, "not_sweep001"), None);
6055    }
6056
6057    #[track_caller]
6058    fn expect_sketch(object: &Object) -> &Sketch {
6059        if let ObjectKind::Sketch(sketch) = &object.kind {
6060            sketch
6061        } else {
6062            panic!("Object is not a sketch: {:?}", object);
6063        }
6064    }
6065
6066    fn point_position(scene_graph: &SceneGraph, point_id: ObjectId) -> Point2d<Number> {
6067        let point_object = scene_graph.objects.get(point_id.0).unwrap();
6068        let ObjectKind::Segment {
6069            segment: Segment::Point(point),
6070        } = &point_object.kind
6071        else {
6072            panic!("Object is not a point segment: {point_object:?}");
6073        };
6074        point.position.clone()
6075    }
6076
6077    fn assert_point_position_close(actual: Point2d<Number>, expected: Point2d<Number>) {
6078        assert!((actual.x.value - expected.x.value).abs() < 1e-6);
6079        assert!((actual.y.value - expected.y.value).abs() < 1e-6);
6080    }
6081
6082    fn make_line_ctor(start_x: f64, start_y: f64, end_x: f64, end_y: f64, units: NumericSuffix) -> LineCtor {
6083        LineCtor {
6084            start: Point2d {
6085                x: Expr::Number(Number { value: start_x, units }),
6086                y: Expr::Number(Number { value: start_y, units }),
6087            },
6088            end: Point2d {
6089                x: Expr::Number(Number { value: end_x, units }),
6090                y: Expr::Number(Number { value: end_y, units }),
6091            },
6092            construction: None,
6093        }
6094    }
6095
6096    async fn create_sketch_with_single_line(
6097        frontend: &mut FrontendState,
6098        ctx: &ExecutorContext,
6099        mock_ctx: &ExecutorContext,
6100        version: Version,
6101    ) -> (ObjectId, ObjectId, SourceDelta, SceneGraphDelta) {
6102        frontend.program = Program::empty();
6103
6104        let sketch_args = SketchCtor {
6105            on: Plane::Default(PlaneName::Xy),
6106        };
6107        let (_src_delta, _scene_delta, sketch_id) = frontend
6108            .new_sketch(ctx, ProjectId(0), FileId(0), version, sketch_args)
6109            .await
6110            .unwrap();
6111
6112        let segment = SegmentCtor::Line(make_line_ctor(0.0, 0.0, 10.0, 10.0, NumericSuffix::Mm));
6113        let (source_delta, scene_graph_delta) = frontend
6114            .add_segment(mock_ctx, version, sketch_id, segment, None)
6115            .await
6116            .unwrap();
6117        let line_id = *scene_graph_delta
6118            .new_objects
6119            .last()
6120            .expect("Expected line object id to be created");
6121
6122        (sketch_id, line_id, source_delta, scene_graph_delta)
6123    }
6124
6125    #[tokio::test(flavor = "multi_thread")]
6126    async fn test_sketch_checkpoint_round_trip_restores_state() {
6127        let mut frontend = FrontendState::new();
6128        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6129        let mock_ctx = ExecutorContext::new_mock(None).await;
6130        let version = Version(0);
6131
6132        let (sketch_id, line_id, source_delta, scene_graph_delta) =
6133            create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
6134
6135        let expected_source = source_delta.text.clone();
6136        let expected_scene_graph = frontend.scene_graph.clone();
6137        let expected_exec_outcome = scene_graph_delta.exec_outcome.clone();
6138        let expected_point_freedom_cache = frontend.point_freedom_cache.clone();
6139
6140        let checkpoint_id = frontend
6141            .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
6142            .await
6143            .unwrap();
6144
6145        let edited_segments = vec![ExistingSegmentCtor {
6146            id: line_id,
6147            ctor: SegmentCtor::Line(make_line_ctor(1.0, 2.0, 13.0, 14.0, NumericSuffix::Mm)),
6148        }];
6149        let (edited_source, _edited_scene) = frontend
6150            .edit_segments(&mock_ctx, version, sketch_id, edited_segments)
6151            .await
6152            .unwrap();
6153        assert_ne!(edited_source.text, expected_source);
6154
6155        let restored = frontend.restore_sketch_checkpoint(checkpoint_id).await.unwrap();
6156
6157        assert_eq!(restored.source_delta.text, expected_source);
6158        assert_eq!(restored.scene_graph_delta.new_graph, expected_scene_graph);
6159        assert!(restored.scene_graph_delta.invalidates_ids);
6160        assert_eq!(restored.scene_graph_delta.exec_outcome, expected_exec_outcome);
6161        assert_eq!(frontend.scene_graph, expected_scene_graph);
6162        assert_eq!(frontend.point_freedom_cache, expected_point_freedom_cache);
6163
6164        ctx.close().await;
6165        mock_ctx.close().await;
6166    }
6167
6168    #[tokio::test(flavor = "multi_thread")]
6169    async fn test_sketch_checkpoints_prune_oldest_entries() {
6170        let mut frontend = FrontendState::new();
6171        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6172        let mock_ctx = ExecutorContext::new_mock(None).await;
6173        let version = Version(0);
6174
6175        let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
6176            create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
6177
6178        let mut checkpoint_ids = Vec::new();
6179        for _ in 0..(MAX_SKETCH_CHECKPOINTS + 3) {
6180            checkpoint_ids.push(
6181                frontend
6182                    .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
6183                    .await
6184                    .unwrap(),
6185            );
6186        }
6187
6188        assert_eq!(frontend.sketch_checkpoints.len(), MAX_SKETCH_CHECKPOINTS);
6189        assert!(checkpoint_ids.windows(2).all(|ids| ids[0] < ids[1]));
6190
6191        let oldest_retained = checkpoint_ids[3];
6192        assert_eq!(
6193            frontend.sketch_checkpoints.front().map(|checkpoint| checkpoint.id),
6194            Some(oldest_retained)
6195        );
6196
6197        let evicted_restore = frontend.restore_sketch_checkpoint(checkpoint_ids[0]).await;
6198        assert!(evicted_restore.is_err());
6199        assert!(evicted_restore.unwrap_err().msg.contains("Sketch checkpoint not found"));
6200
6201        frontend
6202            .restore_sketch_checkpoint(*checkpoint_ids.last().unwrap())
6203            .await
6204            .unwrap();
6205
6206        ctx.close().await;
6207        mock_ctx.close().await;
6208    }
6209
6210    #[tokio::test(flavor = "multi_thread")]
6211    async fn test_restore_sketch_checkpoint_missing_id_returns_error() {
6212        let mut frontend = FrontendState::new();
6213        let missing_checkpoint = SketchCheckpointId::new(999);
6214
6215        let err = frontend
6216            .restore_sketch_checkpoint(missing_checkpoint)
6217            .await
6218            .expect_err("Expected restore to fail for missing checkpoint");
6219
6220        assert!(err.msg.contains("Sketch checkpoint not found"));
6221    }
6222
6223    #[tokio::test(flavor = "multi_thread")]
6224    async fn test_clear_sketch_checkpoints_removes_all_restore_points() {
6225        let mut frontend = FrontendState::new();
6226        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6227        let mock_ctx = ExecutorContext::new_mock(None).await;
6228        let version = Version(0);
6229
6230        let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
6231            create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
6232
6233        let checkpoint_a = frontend
6234            .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
6235            .await
6236            .unwrap();
6237        let checkpoint_b = frontend
6238            .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
6239            .await
6240            .unwrap();
6241        assert_eq!(frontend.sketch_checkpoints.len(), 2);
6242
6243        frontend.clear_sketch_checkpoints();
6244        assert!(frontend.sketch_checkpoints.is_empty());
6245        frontend.restore_sketch_checkpoint(checkpoint_a).await.unwrap_err();
6246        frontend.restore_sketch_checkpoint(checkpoint_b).await.unwrap_err();
6247
6248        ctx.close().await;
6249        mock_ctx.close().await;
6250    }
6251
6252    #[tokio::test(flavor = "multi_thread")]
6253    async fn test_hack_set_program_keeps_old_checkpoints_and_adds_fresh_baseline() {
6254        let mut frontend = FrontendState::new();
6255        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6256        let mock_ctx = ExecutorContext::new_mock(None).await;
6257        let version = Version(0);
6258
6259        let (_sketch_id, _line_id, source_delta, scene_graph_delta) =
6260            create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
6261        let old_source = source_delta.text.clone();
6262        let old_checkpoint = frontend
6263            .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
6264            .await
6265            .unwrap();
6266        let initial_checkpoint_count = frontend.sketch_checkpoints.len();
6267
6268        let new_program = Program::parse("sketch(on = XY) {\n  point(at = [1mm, 2mm])\n}\n")
6269            .unwrap()
6270            .0
6271            .unwrap();
6272
6273        let result = frontend.hack_set_program(&ctx, new_program).await.unwrap();
6274        let SetProgramOutcome::Success {
6275            checkpoint_id: Some(new_checkpoint),
6276            ..
6277        } = result
6278        else {
6279            panic!("Expected Success with a fresh checkpoint baseline");
6280        };
6281
6282        assert_eq!(frontend.sketch_checkpoints.len(), initial_checkpoint_count + 1);
6283
6284        let old_restore = frontend.restore_sketch_checkpoint(old_checkpoint).await.unwrap();
6285        assert_eq!(old_restore.source_delta.text, old_source);
6286
6287        let new_restore = frontend.restore_sketch_checkpoint(new_checkpoint).await.unwrap();
6288        assert!(new_restore.source_delta.text.contains("point(at = [1mm, 2mm])"));
6289
6290        ctx.close().await;
6291        mock_ctx.close().await;
6292    }
6293
6294    #[tokio::test(flavor = "multi_thread")]
6295    async fn test_hack_set_program_exec_failure_does_not_add_checkpoint() {
6296        let mut frontend = FrontendState::new();
6297        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6298        let mock_ctx = ExecutorContext::new_mock(None).await;
6299        let version = Version(0);
6300
6301        let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
6302            create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
6303        let old_checkpoint = frontend
6304            .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
6305            .await
6306            .unwrap();
6307        let checkpoint_count_before = frontend.sketch_checkpoints.len();
6308
6309        let failing_program = Program::parse(
6310            "sketch(on = XY) {\n  line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])\n}\n\nbad = missing_name\n",
6311        )
6312        .unwrap()
6313        .0
6314        .unwrap();
6315
6316        let result = frontend.hack_set_program(&ctx, failing_program).await.unwrap();
6317        assert!(matches!(result, SetProgramOutcome::ExecFailure { .. }));
6318        assert_eq!(frontend.sketch_checkpoints.len(), checkpoint_count_before);
6319        frontend.restore_sketch_checkpoint(old_checkpoint).await.unwrap();
6320
6321        ctx.close().await;
6322        mock_ctx.close().await;
6323    }
6324
6325    #[tokio::test(flavor = "multi_thread")]
6326    async fn test_restore_sketch_checkpoint_restores_and_clears_mock_memory() {
6327        let mut frontend = FrontendState::new();
6328        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6329
6330        let program = Program::parse(
6331            "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",
6332        )
6333        .unwrap()
6334        .0
6335        .unwrap();
6336        let set_program_outcome = frontend.hack_set_program(&ctx, program).await.unwrap();
6337        let SetProgramOutcome::Success { exec_outcome, .. } = set_program_outcome else {
6338            panic!("Expected successful baseline program execution");
6339        };
6340
6341        clear_mem_cache().await;
6342        assert!(read_old_memory().await.is_none());
6343
6344        let checkpoint_without_mock_memory = frontend
6345            .create_sketch_checkpoint((*exec_outcome).clone())
6346            .await
6347            .unwrap();
6348
6349        write_old_memory(SketchModeState::new_for_tests()).await;
6350        assert!(read_old_memory().await.is_some());
6351
6352        let checkpoint_with_mock_memory = frontend
6353            .create_sketch_checkpoint((*exec_outcome).clone())
6354            .await
6355            .unwrap();
6356
6357        clear_mem_cache().await;
6358        assert!(read_old_memory().await.is_none());
6359
6360        frontend
6361            .restore_sketch_checkpoint(checkpoint_with_mock_memory)
6362            .await
6363            .unwrap();
6364        assert!(read_old_memory().await.is_some());
6365
6366        frontend
6367            .restore_sketch_checkpoint(checkpoint_without_mock_memory)
6368            .await
6369            .unwrap();
6370        assert!(read_old_memory().await.is_none());
6371
6372        ctx.close().await;
6373    }
6374
6375    #[tokio::test(flavor = "multi_thread")]
6376    async fn test_hack_set_program_exec_error_still_allows_edit_sketch() {
6377        let source = "\
6378sketch(on = XY) {
6379  line1 = line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])
6380}
6381
6382bad = missing_name
6383";
6384        let program = Program::parse(source).unwrap().0.unwrap();
6385
6386        let mut frontend = FrontendState::new();
6387
6388        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6389        let mock_ctx = ExecutorContext::new_mock(None).await;
6390        let version = Version(0);
6391        let project_id = ProjectId(0);
6392        let file_id = FileId(0);
6393
6394        let SetProgramOutcome::ExecFailure { .. } = frontend.hack_set_program(&ctx, program).await.unwrap() else {
6395            panic!("Expected ExecFailure from hack_set_program due to syntax error in program");
6396        };
6397
6398        let sketch_id = frontend
6399            .scene_graph
6400            .objects
6401            .iter()
6402            .find_map(|obj| matches!(obj.kind, ObjectKind::Sketch(_)).then_some(obj.id))
6403            .expect("Expected sketch object from errored hack_set_program");
6404
6405        frontend
6406            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
6407            .await
6408            .unwrap();
6409
6410        ctx.close().await;
6411        mock_ctx.close().await;
6412    }
6413
6414    #[tokio::test(flavor = "multi_thread")]
6415    async fn test_new_sketch_add_point_edit_point() {
6416        let program = Program::empty();
6417
6418        let mut frontend = FrontendState::new();
6419        frontend.program = program;
6420
6421        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6422        let mock_ctx = ExecutorContext::new_mock(None).await;
6423        let version = Version(0);
6424
6425        let sketch_args = SketchCtor {
6426            on: Plane::Default(PlaneName::Xy),
6427        };
6428        let (_src_delta, scene_delta, sketch_id) = frontend
6429            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
6430            .await
6431            .unwrap();
6432        assert_eq!(sketch_id, ObjectId(1));
6433        assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
6434        let sketch_object = &scene_delta.new_graph.objects[1];
6435        assert_eq!(sketch_object.id, ObjectId(1));
6436        assert_eq!(
6437            sketch_object.kind,
6438            ObjectKind::Sketch(Sketch {
6439                args: SketchCtor {
6440                    on: Plane::Default(PlaneName::Xy)
6441                },
6442                plane: ObjectId(0),
6443                segments: vec![],
6444                constraints: vec![],
6445            })
6446        );
6447        assert_eq!(scene_delta.new_graph.objects.len(), 2);
6448
6449        let point_ctor = PointCtor {
6450            position: Point2d {
6451                x: Expr::Number(Number {
6452                    value: 1.0,
6453                    units: NumericSuffix::Inch,
6454                }),
6455                y: Expr::Number(Number {
6456                    value: 2.0,
6457                    units: NumericSuffix::Inch,
6458                }),
6459            },
6460        };
6461        let segment = SegmentCtor::Point(point_ctor);
6462        let (src_delta, scene_delta) = frontend
6463            .add_segment(&mock_ctx, version, sketch_id, segment, None)
6464            .await
6465            .unwrap();
6466        assert_eq!(
6467            src_delta.text.as_str(),
6468            "sketch001 = sketch(on = XY) {
6469  point(at = [1in, 2in])
6470}
6471"
6472        );
6473        assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
6474        assert_eq!(scene_delta.new_graph.objects.len(), 3);
6475        for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
6476            assert_eq!(scene_object.id.0, i);
6477        }
6478
6479        let point_id = *scene_delta.new_objects.last().unwrap();
6480
6481        let point_ctor = PointCtor {
6482            position: Point2d {
6483                x: Expr::Number(Number {
6484                    value: 3.0,
6485                    units: NumericSuffix::Inch,
6486                }),
6487                y: Expr::Number(Number {
6488                    value: 4.0,
6489                    units: NumericSuffix::Inch,
6490                }),
6491            },
6492        };
6493        let segments = vec![ExistingSegmentCtor {
6494            id: point_id,
6495            ctor: SegmentCtor::Point(point_ctor),
6496        }];
6497        let (src_delta, scene_delta) = frontend
6498            .edit_segments(&mock_ctx, version, sketch_id, segments)
6499            .await
6500            .unwrap();
6501        assert_eq!(
6502            src_delta.text.as_str(),
6503            "sketch001 = sketch(on = XY) {
6504  point(at = [3in, 4in])
6505}
6506"
6507        );
6508        assert_eq!(scene_delta.new_objects, vec![]);
6509        assert_eq!(scene_delta.new_graph.objects.len(), 3);
6510
6511        ctx.close().await;
6512        mock_ctx.close().await;
6513    }
6514
6515    #[tokio::test(flavor = "multi_thread")]
6516    async fn test_new_sketch_add_line_edit_line() {
6517        let program = Program::empty();
6518
6519        let mut frontend = FrontendState::new();
6520        frontend.program = program;
6521
6522        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6523        let mock_ctx = ExecutorContext::new_mock(None).await;
6524        let version = Version(0);
6525
6526        let sketch_args = SketchCtor {
6527            on: Plane::Default(PlaneName::Xy),
6528        };
6529        let (_src_delta, scene_delta, sketch_id) = frontend
6530            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
6531            .await
6532            .unwrap();
6533        assert_eq!(sketch_id, ObjectId(1));
6534        assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
6535        let sketch_object = &scene_delta.new_graph.objects[1];
6536        assert_eq!(sketch_object.id, ObjectId(1));
6537        assert_eq!(
6538            sketch_object.kind,
6539            ObjectKind::Sketch(Sketch {
6540                args: SketchCtor {
6541                    on: Plane::Default(PlaneName::Xy)
6542                },
6543                plane: ObjectId(0),
6544                segments: vec![],
6545                constraints: vec![],
6546            })
6547        );
6548        assert_eq!(scene_delta.new_graph.objects.len(), 2);
6549
6550        let line_ctor = LineCtor {
6551            start: Point2d {
6552                x: Expr::Number(Number {
6553                    value: 0.0,
6554                    units: NumericSuffix::Mm,
6555                }),
6556                y: Expr::Number(Number {
6557                    value: 0.0,
6558                    units: NumericSuffix::Mm,
6559                }),
6560            },
6561            end: Point2d {
6562                x: Expr::Number(Number {
6563                    value: 10.0,
6564                    units: NumericSuffix::Mm,
6565                }),
6566                y: Expr::Number(Number {
6567                    value: 10.0,
6568                    units: NumericSuffix::Mm,
6569                }),
6570            },
6571            construction: None,
6572        };
6573        let segment = SegmentCtor::Line(line_ctor);
6574        let (src_delta, scene_delta) = frontend
6575            .add_segment(&mock_ctx, version, sketch_id, segment, None)
6576            .await
6577            .unwrap();
6578        assert_eq!(
6579            src_delta.text.as_str(),
6580            "sketch001 = sketch(on = XY) {
6581  line(start = [0mm, 0mm], end = [10mm, 10mm])
6582}
6583"
6584        );
6585        assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
6586        assert_eq!(scene_delta.new_graph.objects.len(), 5);
6587        for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
6588            assert_eq!(scene_object.id.0, i);
6589        }
6590
6591        // The new objects are the end points and then the line.
6592        let line = *scene_delta.new_objects.last().unwrap();
6593
6594        let line_ctor = LineCtor {
6595            start: Point2d {
6596                x: Expr::Number(Number {
6597                    value: 1.0,
6598                    units: NumericSuffix::Mm,
6599                }),
6600                y: Expr::Number(Number {
6601                    value: 2.0,
6602                    units: NumericSuffix::Mm,
6603                }),
6604            },
6605            end: Point2d {
6606                x: Expr::Number(Number {
6607                    value: 13.0,
6608                    units: NumericSuffix::Mm,
6609                }),
6610                y: Expr::Number(Number {
6611                    value: 14.0,
6612                    units: NumericSuffix::Mm,
6613                }),
6614            },
6615            construction: None,
6616        };
6617        let segments = vec![ExistingSegmentCtor {
6618            id: line,
6619            ctor: SegmentCtor::Line(line_ctor),
6620        }];
6621        let (src_delta, scene_delta) = frontend
6622            .edit_segments(&mock_ctx, version, sketch_id, segments)
6623            .await
6624            .unwrap();
6625        assert_eq!(
6626            src_delta.text.as_str(),
6627            "sketch001 = sketch(on = XY) {
6628  line(start = [1mm, 2mm], end = [13mm, 14mm])
6629}
6630"
6631        );
6632        assert_eq!(scene_delta.new_objects, vec![]);
6633        assert_eq!(scene_delta.new_graph.objects.len(), 5);
6634
6635        ctx.close().await;
6636        mock_ctx.close().await;
6637    }
6638
6639    #[tokio::test(flavor = "multi_thread")]
6640    async fn test_new_sketch_add_arc_edit_arc() {
6641        let program = Program::empty();
6642
6643        let mut frontend = FrontendState::new();
6644        frontend.program = program;
6645
6646        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6647        let mock_ctx = ExecutorContext::new_mock(None).await;
6648        let version = Version(0);
6649
6650        let sketch_args = SketchCtor {
6651            on: Plane::Default(PlaneName::Xy),
6652        };
6653        let (_src_delta, scene_delta, sketch_id) = frontend
6654            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
6655            .await
6656            .unwrap();
6657        assert_eq!(sketch_id, ObjectId(1));
6658        assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
6659        let sketch_object = &scene_delta.new_graph.objects[1];
6660        assert_eq!(sketch_object.id, ObjectId(1));
6661        assert_eq!(
6662            sketch_object.kind,
6663            ObjectKind::Sketch(Sketch {
6664                args: SketchCtor {
6665                    on: Plane::Default(PlaneName::Xy),
6666                },
6667                plane: ObjectId(0),
6668                segments: vec![],
6669                constraints: vec![],
6670            })
6671        );
6672        assert_eq!(scene_delta.new_graph.objects.len(), 2);
6673
6674        let arc_ctor = ArcCtor {
6675            start: Point2d {
6676                x: Expr::Var(Number {
6677                    value: 0.0,
6678                    units: NumericSuffix::Mm,
6679                }),
6680                y: Expr::Var(Number {
6681                    value: 0.0,
6682                    units: NumericSuffix::Mm,
6683                }),
6684            },
6685            end: Point2d {
6686                x: Expr::Var(Number {
6687                    value: 10.0,
6688                    units: NumericSuffix::Mm,
6689                }),
6690                y: Expr::Var(Number {
6691                    value: 10.0,
6692                    units: NumericSuffix::Mm,
6693                }),
6694            },
6695            center: Point2d {
6696                x: Expr::Var(Number {
6697                    value: 10.0,
6698                    units: NumericSuffix::Mm,
6699                }),
6700                y: Expr::Var(Number {
6701                    value: 0.0,
6702                    units: NumericSuffix::Mm,
6703                }),
6704            },
6705            construction: None,
6706        };
6707        let segment = SegmentCtor::Arc(arc_ctor);
6708        let (src_delta, scene_delta) = frontend
6709            .add_segment(&mock_ctx, version, sketch_id, segment, None)
6710            .await
6711            .unwrap();
6712        assert_eq!(
6713            src_delta.text.as_str(),
6714            "sketch001 = sketch(on = XY) {
6715  arc(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm], center = [var 10mm, var 0mm])
6716}
6717"
6718        );
6719        assert_eq!(
6720            scene_delta.new_objects,
6721            vec![ObjectId(2), ObjectId(3), ObjectId(4), ObjectId(5)]
6722        );
6723        for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
6724            assert_eq!(scene_object.id.0, i);
6725        }
6726        assert_eq!(scene_delta.new_graph.objects.len(), 6);
6727
6728        // The new objects are the end points, the center, and then the arc.
6729        let arc = *scene_delta.new_objects.last().unwrap();
6730
6731        let arc_ctor = ArcCtor {
6732            start: Point2d {
6733                x: Expr::Var(Number {
6734                    value: 1.0,
6735                    units: NumericSuffix::Mm,
6736                }),
6737                y: Expr::Var(Number {
6738                    value: 2.0,
6739                    units: NumericSuffix::Mm,
6740                }),
6741            },
6742            end: Point2d {
6743                x: Expr::Var(Number {
6744                    value: 13.0,
6745                    units: NumericSuffix::Mm,
6746                }),
6747                y: Expr::Var(Number {
6748                    value: 14.0,
6749                    units: NumericSuffix::Mm,
6750                }),
6751            },
6752            center: Point2d {
6753                x: Expr::Var(Number {
6754                    value: 13.0,
6755                    units: NumericSuffix::Mm,
6756                }),
6757                y: Expr::Var(Number {
6758                    value: 2.0,
6759                    units: NumericSuffix::Mm,
6760                }),
6761            },
6762            construction: None,
6763        };
6764        let segments = vec![ExistingSegmentCtor {
6765            id: arc,
6766            ctor: SegmentCtor::Arc(arc_ctor),
6767        }];
6768        let (src_delta, scene_delta) = frontend
6769            .edit_segments(&mock_ctx, version, sketch_id, segments)
6770            .await
6771            .unwrap();
6772        assert_eq!(
6773            src_delta.text.as_str(),
6774            "sketch001 = sketch(on = XY) {
6775  arc(start = [var 1mm, var 2mm], end = [var 13mm, var 14mm], center = [var 13mm, var 2mm])
6776}
6777"
6778        );
6779        assert_eq!(scene_delta.new_objects, vec![]);
6780        assert_eq!(scene_delta.new_graph.objects.len(), 6);
6781
6782        ctx.close().await;
6783        mock_ctx.close().await;
6784    }
6785
6786    #[tokio::test(flavor = "multi_thread")]
6787    async fn test_new_sketch_add_circle_edit_circle() {
6788        let program = Program::empty();
6789
6790        let mut frontend = FrontendState::new();
6791        frontend.program = program;
6792
6793        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6794        let mock_ctx = ExecutorContext::new_mock(None).await;
6795        let version = Version(0);
6796
6797        let sketch_args = SketchCtor {
6798            on: Plane::Default(PlaneName::Xy),
6799        };
6800        let (_src_delta, _scene_delta, sketch_id) = frontend
6801            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
6802            .await
6803            .unwrap();
6804
6805        // Add a circle segment.
6806        let circle_ctor = CircleCtor {
6807            start: Point2d {
6808                x: Expr::Var(Number {
6809                    value: 5.0,
6810                    units: NumericSuffix::Mm,
6811                }),
6812                y: Expr::Var(Number {
6813                    value: 0.0,
6814                    units: NumericSuffix::Mm,
6815                }),
6816            },
6817            center: Point2d {
6818                x: Expr::Var(Number {
6819                    value: 0.0,
6820                    units: NumericSuffix::Mm,
6821                }),
6822                y: Expr::Var(Number {
6823                    value: 0.0,
6824                    units: NumericSuffix::Mm,
6825                }),
6826            },
6827            construction: None,
6828        };
6829        let segment = SegmentCtor::Circle(circle_ctor);
6830        let (src_delta, scene_delta) = frontend
6831            .add_segment(&mock_ctx, version, sketch_id, segment, None)
6832            .await
6833            .unwrap();
6834        assert_eq!(
6835            src_delta.text.as_str(),
6836            "sketch001 = sketch(on = XY) {
6837  circle1 = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
6838}
6839"
6840        );
6841        // The new objects are start, center, and then the circle segment.
6842        assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
6843        assert_eq!(scene_delta.new_graph.objects.len(), 5);
6844
6845        let circle = *scene_delta.new_objects.last().unwrap();
6846
6847        // Edit the circle segment.
6848        let circle_ctor = CircleCtor {
6849            start: Point2d {
6850                x: Expr::Var(Number {
6851                    value: 10.0,
6852                    units: NumericSuffix::Mm,
6853                }),
6854                y: Expr::Var(Number {
6855                    value: 0.0,
6856                    units: NumericSuffix::Mm,
6857                }),
6858            },
6859            center: Point2d {
6860                x: Expr::Var(Number {
6861                    value: 3.0,
6862                    units: NumericSuffix::Mm,
6863                }),
6864                y: Expr::Var(Number {
6865                    value: 4.0,
6866                    units: NumericSuffix::Mm,
6867                }),
6868            },
6869            construction: None,
6870        };
6871        let segments = vec![ExistingSegmentCtor {
6872            id: circle,
6873            ctor: SegmentCtor::Circle(circle_ctor),
6874        }];
6875        let (src_delta, scene_delta) = frontend
6876            .edit_segments(&mock_ctx, version, sketch_id, segments)
6877            .await
6878            .unwrap();
6879        assert_eq!(
6880            src_delta.text.as_str(),
6881            "sketch001 = sketch(on = XY) {
6882  circle1 = circle(start = [var 10mm, var 0mm], center = [var 3mm, var 4mm])
6883}
6884"
6885        );
6886        assert_eq!(scene_delta.new_objects, vec![]);
6887        assert_eq!(scene_delta.new_graph.objects.len(), 5);
6888
6889        ctx.close().await;
6890        mock_ctx.close().await;
6891    }
6892
6893    #[tokio::test(flavor = "multi_thread")]
6894    async fn test_delete_circle() {
6895        let initial_source = "sketch001 = sketch(on = XY) {
6896  circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
6897}
6898";
6899
6900        let program = Program::parse(initial_source).unwrap().0.unwrap();
6901        let mut frontend = FrontendState::new();
6902
6903        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6904        let mock_ctx = ExecutorContext::new_mock(None).await;
6905        let version = Version(0);
6906
6907        frontend.hack_set_program(&ctx, program).await.unwrap();
6908        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
6909        let sketch_id = sketch_object.id;
6910        let sketch = expect_sketch(sketch_object);
6911
6912        // The sketch should have 3 segments: start point, center point, and the circle.
6913        assert_eq!(sketch.segments.len(), 3);
6914        let circle_id = sketch.segments[2];
6915
6916        // Delete the circle.
6917        let (src_delta, scene_delta) = frontend
6918            .delete_objects(&mock_ctx, version, sketch_id, vec![], vec![circle_id])
6919            .await
6920            .unwrap();
6921        assert_eq!(
6922            src_delta.text.as_str(),
6923            "sketch001 = sketch(on = XY) {
6924}
6925"
6926        );
6927        let new_sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
6928        let new_sketch = expect_sketch(new_sketch_object);
6929        assert_eq!(new_sketch.segments.len(), 0);
6930
6931        ctx.close().await;
6932        mock_ctx.close().await;
6933    }
6934
6935    #[tokio::test(flavor = "multi_thread")]
6936    async fn test_edit_circle_via_point() {
6937        let initial_source = "sketch001 = sketch(on = XY) {
6938  circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
6939}
6940";
6941
6942        let program = Program::parse(initial_source).unwrap().0.unwrap();
6943        let mut frontend = FrontendState::new();
6944
6945        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6946        let mock_ctx = ExecutorContext::new_mock(None).await;
6947        let version = Version(0);
6948
6949        frontend.hack_set_program(&ctx, program).await.unwrap();
6950        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
6951        let sketch_id = sketch_object.id;
6952        let sketch = expect_sketch(sketch_object);
6953
6954        // Find the circle segment and its start point.
6955        let circle_id = sketch
6956            .segments
6957            .iter()
6958            .copied()
6959            .find(|seg_id| {
6960                matches!(
6961                    &frontend.scene_graph.objects[seg_id.0].kind,
6962                    ObjectKind::Segment {
6963                        segment: Segment::Circle(_)
6964                    }
6965                )
6966            })
6967            .expect("Expected a circle segment in sketch");
6968        let circle_object = &frontend.scene_graph.objects[circle_id.0];
6969        let ObjectKind::Segment {
6970            segment: Segment::Circle(circle),
6971        } = &circle_object.kind
6972        else {
6973            panic!("Expected circle segment, got: {:?}", circle_object.kind);
6974        };
6975        let start_point_id = circle.start;
6976
6977        // Edit the start point via SegmentCtor::Point.
6978        let segments = vec![ExistingSegmentCtor {
6979            id: start_point_id,
6980            ctor: SegmentCtor::Point(PointCtor {
6981                position: Point2d {
6982                    x: Expr::Var(Number {
6983                        value: 7.0,
6984                        units: NumericSuffix::Mm,
6985                    }),
6986                    y: Expr::Var(Number {
6987                        value: 1.0,
6988                        units: NumericSuffix::Mm,
6989                    }),
6990                },
6991            }),
6992        }];
6993        let (src_delta, _scene_delta) = frontend
6994            .edit_segments(&mock_ctx, version, sketch_id, segments)
6995            .await
6996            .unwrap();
6997        assert_eq!(
6998            src_delta.text.as_str(),
6999            "sketch001 = sketch(on = XY) {
7000  circle(start = [var 7mm, var 1mm], center = [var 0mm, var 0mm])
7001}
7002"
7003        );
7004
7005        ctx.close().await;
7006        mock_ctx.close().await;
7007    }
7008
7009    #[tokio::test(flavor = "multi_thread")]
7010    async fn test_add_line_when_sketch_block_uses_variable() {
7011        let initial_source = "s = sketch(on = XY) {}
7012";
7013
7014        let program = Program::parse(initial_source).unwrap().0.unwrap();
7015
7016        let mut frontend = FrontendState::new();
7017
7018        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7019        let mock_ctx = ExecutorContext::new_mock(None).await;
7020        let version = Version(0);
7021
7022        frontend.hack_set_program(&ctx, program).await.unwrap();
7023        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7024        let sketch_id = sketch_object.id;
7025
7026        let line_ctor = LineCtor {
7027            start: Point2d {
7028                x: Expr::Number(Number {
7029                    value: 0.0,
7030                    units: NumericSuffix::Mm,
7031                }),
7032                y: Expr::Number(Number {
7033                    value: 0.0,
7034                    units: NumericSuffix::Mm,
7035                }),
7036            },
7037            end: Point2d {
7038                x: Expr::Number(Number {
7039                    value: 10.0,
7040                    units: NumericSuffix::Mm,
7041                }),
7042                y: Expr::Number(Number {
7043                    value: 10.0,
7044                    units: NumericSuffix::Mm,
7045                }),
7046            },
7047            construction: None,
7048        };
7049        let segment = SegmentCtor::Line(line_ctor);
7050        let (src_delta, scene_delta) = frontend
7051            .add_segment(&mock_ctx, version, sketch_id, segment, None)
7052            .await
7053            .unwrap();
7054        assert_eq!(
7055            src_delta.text.as_str(),
7056            "s = sketch(on = XY) {
7057  line(start = [0mm, 0mm], end = [10mm, 10mm])
7058}
7059"
7060        );
7061        assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
7062        assert_eq!(scene_delta.new_graph.objects.len(), 5);
7063
7064        ctx.close().await;
7065        mock_ctx.close().await;
7066    }
7067
7068    #[tokio::test(flavor = "multi_thread")]
7069    async fn test_new_sketch_add_line_delete_sketch() {
7070        let program = Program::empty();
7071
7072        let mut frontend = FrontendState::new();
7073        frontend.program = program;
7074
7075        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7076        let mock_ctx = ExecutorContext::new_mock(None).await;
7077        let version = Version(0);
7078
7079        let sketch_args = SketchCtor {
7080            on: Plane::Default(PlaneName::Xy),
7081        };
7082        let (_src_delta, scene_delta, sketch_id) = frontend
7083            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
7084            .await
7085            .unwrap();
7086        assert_eq!(sketch_id, ObjectId(1));
7087        assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
7088        let sketch_object = &scene_delta.new_graph.objects[1];
7089        assert_eq!(sketch_object.id, ObjectId(1));
7090        assert_eq!(
7091            sketch_object.kind,
7092            ObjectKind::Sketch(Sketch {
7093                args: SketchCtor {
7094                    on: Plane::Default(PlaneName::Xy)
7095                },
7096                plane: ObjectId(0),
7097                segments: vec![],
7098                constraints: vec![],
7099            })
7100        );
7101        assert_eq!(scene_delta.new_graph.objects.len(), 2);
7102
7103        let line_ctor = LineCtor {
7104            start: Point2d {
7105                x: Expr::Number(Number {
7106                    value: 0.0,
7107                    units: NumericSuffix::Mm,
7108                }),
7109                y: Expr::Number(Number {
7110                    value: 0.0,
7111                    units: NumericSuffix::Mm,
7112                }),
7113            },
7114            end: Point2d {
7115                x: Expr::Number(Number {
7116                    value: 10.0,
7117                    units: NumericSuffix::Mm,
7118                }),
7119                y: Expr::Number(Number {
7120                    value: 10.0,
7121                    units: NumericSuffix::Mm,
7122                }),
7123            },
7124            construction: None,
7125        };
7126        let segment = SegmentCtor::Line(line_ctor);
7127        let (src_delta, scene_delta) = frontend
7128            .add_segment(&mock_ctx, version, sketch_id, segment, None)
7129            .await
7130            .unwrap();
7131        assert_eq!(
7132            src_delta.text.as_str(),
7133            "sketch001 = sketch(on = XY) {
7134  line(start = [0mm, 0mm], end = [10mm, 10mm])
7135}
7136"
7137        );
7138        assert_eq!(scene_delta.new_graph.objects.len(), 5);
7139
7140        let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
7141        assert_eq!(src_delta.text.as_str(), "");
7142        assert_eq!(scene_delta.new_graph.objects.len(), 0);
7143
7144        ctx.close().await;
7145        mock_ctx.close().await;
7146    }
7147
7148    #[tokio::test(flavor = "multi_thread")]
7149    async fn test_delete_sketch_when_sketch_block_uses_variable() {
7150        let initial_source = "s = sketch(on = XY) {}
7151";
7152
7153        let program = Program::parse(initial_source).unwrap().0.unwrap();
7154
7155        let mut frontend = FrontendState::new();
7156
7157        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7158        let mock_ctx = ExecutorContext::new_mock(None).await;
7159        let version = Version(0);
7160
7161        frontend.hack_set_program(&ctx, program).await.unwrap();
7162        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7163        let sketch_id = sketch_object.id;
7164
7165        let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
7166        assert_eq!(src_delta.text.as_str(), "");
7167        assert_eq!(scene_delta.new_graph.objects.len(), 0);
7168
7169        ctx.close().await;
7170        mock_ctx.close().await;
7171    }
7172
7173    #[tokio::test(flavor = "multi_thread")]
7174    async fn test_delete_sketch_after_comment() {
7175        let initial_source = "sketch001 = sketch(on = XZ) {
7176}
7177";
7178
7179        let program = Program::parse(initial_source).unwrap().0.unwrap();
7180        let mut frontend = FrontendState::new();
7181
7182        let ctx = ExecutorContext::new_with_engine(
7183            std::sync::Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().unwrap())),
7184            Default::default(),
7185        );
7186        let version = Version(0);
7187
7188        frontend.hack_set_program(&ctx, program).await.unwrap();
7189        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7190        let sketch_id = sketch_object.id;
7191        let original_source = sketch_object.source.clone();
7192
7193        let commented_source = "// test 1
7194sketch001 = sketch(on = XZ) {
7195}
7196";
7197        let commented_program = Program::parse(commented_source).unwrap().0.unwrap();
7198        frontend.engine_execute(&ctx, commented_program).await.unwrap();
7199
7200        let cached_sketch_object = &frontend.scene_graph.objects[sketch_id.0];
7201        assert_eq!(cached_sketch_object.source, original_source);
7202
7203        let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
7204        assert!(
7205            !src_delta.text.contains("sketch001"),
7206            "sketch was not deleted: {}",
7207            src_delta.text
7208        );
7209        // The leading line comment must survive deletion.
7210        assert_eq!(src_delta.text.as_str(), "// test 1\n");
7211        assert_eq!(scene_delta.new_graph.objects.len(), 0);
7212
7213        ctx.close().await;
7214    }
7215
7216    #[tokio::test(flavor = "multi_thread")]
7217    async fn test_delete_sketch_preserves_pre_comment_when_followed_by_code() {
7218        let initial_source = "sketch001 = sketch(on = XZ) {
7219}
7220foo = 1
7221";
7222
7223        let program = Program::parse(initial_source).unwrap().0.unwrap();
7224        let mut frontend = FrontendState::new();
7225
7226        let ctx = ExecutorContext::new_with_engine(
7227            std::sync::Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().unwrap())),
7228            Default::default(),
7229        );
7230        let version = Version(0);
7231
7232        frontend.hack_set_program(&ctx, program).await.unwrap();
7233        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7234        let sketch_id = sketch_object.id;
7235
7236        let commented_source = "// keep me
7237sketch001 = sketch(on = XZ) {
7238}
7239foo = 1
7240";
7241        let commented_program = Program::parse(commented_source).unwrap().0.unwrap();
7242        frontend.engine_execute(&ctx, commented_program).await.unwrap();
7243
7244        let (src_delta, _scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
7245        // The leading comment should remain, now attached to the following body item.
7246        assert_eq!(src_delta.text.as_str(), "// keep me\nfoo = 1\n");
7247
7248        ctx.close().await;
7249    }
7250
7251    #[tokio::test(flavor = "multi_thread")]
7252    async fn test_delete_segment_preserves_pre_comment() {
7253        let initial_source = "\
7254sketch(on = XY) {
7255  point(at = [var 1, var 2])
7256  // describe the middle point
7257  point(at = [var 3, var 4])
7258  point(at = [var 5, var 6])
7259}
7260";
7261
7262        let program = Program::parse(initial_source).unwrap().0.unwrap();
7263        let mut frontend = FrontendState::new();
7264
7265        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7266        let mock_ctx = ExecutorContext::new_mock(None).await;
7267        let version = Version(0);
7268
7269        frontend.hack_set_program(&ctx, program).await.unwrap();
7270        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7271        let sketch_id = sketch_object.id;
7272        let sketch = expect_sketch(sketch_object);
7273
7274        let middle_point_id = *sketch.segments.get(1).unwrap();
7275
7276        let (src_delta, _scene_delta) = frontend
7277            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![middle_point_id])
7278            .await
7279            .unwrap();
7280        // The line comment on the line above the deleted point must be preserved.
7281        // It is reattached to the next surviving body item.
7282        assert_eq!(
7283            src_delta.text.as_str(),
7284            "\
7285sketch(on = XY) {
7286  point(at = [var 1mm, var 2mm])
7287  // describe the middle point
7288  point(at = [var 5mm, var 6mm])
7289}
7290"
7291        );
7292
7293        ctx.close().await;
7294        mock_ctx.close().await;
7295    }
7296
7297    #[tokio::test(flavor = "multi_thread")]
7298    async fn test_delete_last_segment_preserves_pre_comment() {
7299        let initial_source = "\
7300sketch(on = XY) {
7301  point(at = [var 1, var 2])
7302  // describe the trailing point
7303  point(at = [var 3, var 4])
7304}
7305";
7306
7307        let program = Program::parse(initial_source).unwrap().0.unwrap();
7308        let mut frontend = FrontendState::new();
7309
7310        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7311        let mock_ctx = ExecutorContext::new_mock(None).await;
7312        let version = Version(0);
7313
7314        frontend.hack_set_program(&ctx, program).await.unwrap();
7315        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7316        let sketch_id = sketch_object.id;
7317        let sketch = expect_sketch(sketch_object);
7318
7319        let last_point_id = *sketch.segments.last().unwrap();
7320
7321        let (src_delta, _scene_delta) = frontend
7322            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![last_point_id])
7323            .await
7324            .unwrap();
7325        // No following item to attach to; the comment is kept inside the sketch
7326        // block as trailing non-code metadata so the user does not lose it.
7327        assert_eq!(
7328            src_delta.text.as_str(),
7329            "\
7330sketch(on = XY) {
7331  point(at = [var 1mm, var 2mm])
7332  // describe the trailing point
7333}
7334"
7335        );
7336
7337        ctx.close().await;
7338        mock_ctx.close().await;
7339    }
7340
7341    #[tokio::test(flavor = "multi_thread")]
7342    async fn test_delete_segment_drops_inline_trailing_comment() {
7343        let initial_source = "\
7344sketch(on = XY) {
7345  point(at = [var 1, var 2])
7346  point(at = [var 3, var 4]) // same-line note that gets dropped
7347  point(at = [var 5, var 6])
7348}
7349";
7350
7351        let program = Program::parse(initial_source).unwrap().0.unwrap();
7352        let mut frontend = FrontendState::new();
7353
7354        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7355        let mock_ctx = ExecutorContext::new_mock(None).await;
7356        let version = Version(0);
7357
7358        frontend.hack_set_program(&ctx, program).await.unwrap();
7359        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7360        let sketch_id = sketch_object.id;
7361        let sketch = expect_sketch(sketch_object);
7362
7363        let middle_point_id = *sketch.segments.get(1).unwrap();
7364
7365        let (src_delta, _scene_delta) = frontend
7366            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![middle_point_id])
7367            .await
7368            .unwrap();
7369        // The same-line trailing comment is removed along with the deleted code.
7370        assert!(
7371            !src_delta.text.contains("same-line note"),
7372            "inline comment should have been removed: {}",
7373            src_delta.text
7374        );
7375
7376        ctx.close().await;
7377        mock_ctx.close().await;
7378    }
7379
7380    #[tokio::test(flavor = "multi_thread")]
7381    async fn test_delete_segments_preserves_block_comments_across_positions() {
7382        // One test exercising several `delete_body_item_preserving_pre_comments`
7383        // branches at once with `/* ... */` block comments:
7384        //   - first point: leading block comment must migrate to the next item.
7385        //   - first point: same-line trailing block comment must be dropped.
7386        //   - middle point: leading block comment must stay attached after migration.
7387        //   - last point: leading block comment, with no surviving next item,
7388        //     must be converted into a trailing NonCodeNode.
7389        let initial_source = "\
7390sketch(on = XY) {
7391  /* above first - moves to middle */
7392  point(at = [var 1, var 2]) /* same-line on first - dropped */
7393  /* above middle - stays */
7394  point(at = [var 3, var 4])
7395  /* above last - moves to trailing meta */
7396  point(at = [var 5, var 6])
7397}
7398";
7399
7400        let program = Program::parse(initial_source).unwrap().0.unwrap();
7401        let mut frontend = FrontendState::new();
7402
7403        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7404        let mock_ctx = ExecutorContext::new_mock(None).await;
7405        let version = Version(0);
7406
7407        frontend.hack_set_program(&ctx, program).await.unwrap();
7408        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7409        let sketch_id = sketch_object.id;
7410        let sketch = expect_sketch(sketch_object);
7411
7412        let first_point_id = *sketch.segments.first().unwrap();
7413        let last_point_id = *sketch.segments.last().unwrap();
7414
7415        let (src_delta, _scene_delta) = frontend
7416            .delete_objects(
7417                &mock_ctx,
7418                version,
7419                sketch_id,
7420                Vec::new(),
7421                vec![first_point_id, last_point_id],
7422            )
7423            .await
7424            .unwrap();
7425        assert_eq!(
7426            src_delta.text.as_str(),
7427            "\
7428sketch(on = XY) {
7429  /* above first - moves to middle */
7430  /* above middle - stays */
7431  point(at = [var 3mm, var 4mm])
7432  /* above last - moves to trailing meta */
7433}
7434"
7435        );
7436
7437        ctx.close().await;
7438        mock_ctx.close().await;
7439    }
7440
7441    #[tokio::test(flavor = "multi_thread")]
7442    async fn test_edit_line_when_editing_its_start_point() {
7443        let initial_source = "\
7444sketch(on = XY) {
7445  line(start = [var 1, var 2], end = [var 3, var 4])
7446}
7447";
7448
7449        let program = Program::parse(initial_source).unwrap().0.unwrap();
7450
7451        let mut frontend = FrontendState::new();
7452
7453        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7454        let mock_ctx = ExecutorContext::new_mock(None).await;
7455        let version = Version(0);
7456
7457        frontend.hack_set_program(&ctx, program).await.unwrap();
7458        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7459        let sketch_id = sketch_object.id;
7460        let sketch = expect_sketch(sketch_object);
7461
7462        let point_id = *sketch.segments.first().unwrap();
7463
7464        let point_ctor = PointCtor {
7465            position: Point2d {
7466                x: Expr::Var(Number {
7467                    value: 5.0,
7468                    units: NumericSuffix::Inch,
7469                }),
7470                y: Expr::Var(Number {
7471                    value: 6.0,
7472                    units: NumericSuffix::Inch,
7473                }),
7474            },
7475        };
7476        let segments = vec![ExistingSegmentCtor {
7477            id: point_id,
7478            ctor: SegmentCtor::Point(point_ctor),
7479        }];
7480        let (src_delta, scene_delta) = frontend
7481            .edit_segments(&mock_ctx, version, sketch_id, segments)
7482            .await
7483            .unwrap();
7484        assert_eq!(
7485            src_delta.text.as_str(),
7486            "\
7487sketch(on = XY) {
7488  line(start = [var 127mm, var 152.4mm], end = [var 3mm, var 4mm])
7489}
7490"
7491        );
7492        assert_eq!(scene_delta.new_objects, vec![]);
7493        assert_eq!(scene_delta.new_graph.objects.len(), 5);
7494
7495        ctx.close().await;
7496        mock_ctx.close().await;
7497    }
7498
7499    #[tokio::test(flavor = "multi_thread")]
7500    async fn test_edit_line_when_editing_its_end_point() {
7501        let initial_source = "\
7502sketch(on = XY) {
7503  line(start = [var 1, var 2], end = [var 3, var 4])
7504}
7505";
7506
7507        let program = Program::parse(initial_source).unwrap().0.unwrap();
7508
7509        let mut frontend = FrontendState::new();
7510
7511        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7512        let mock_ctx = ExecutorContext::new_mock(None).await;
7513        let version = Version(0);
7514
7515        frontend.hack_set_program(&ctx, program).await.unwrap();
7516        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7517        let sketch_id = sketch_object.id;
7518        let sketch = expect_sketch(sketch_object);
7519        let point_id = *sketch.segments.get(1).unwrap();
7520
7521        let point_ctor = PointCtor {
7522            position: Point2d {
7523                x: Expr::Var(Number {
7524                    value: 5.0,
7525                    units: NumericSuffix::Inch,
7526                }),
7527                y: Expr::Var(Number {
7528                    value: 6.0,
7529                    units: NumericSuffix::Inch,
7530                }),
7531            },
7532        };
7533        let segments = vec![ExistingSegmentCtor {
7534            id: point_id,
7535            ctor: SegmentCtor::Point(point_ctor),
7536        }];
7537        let (src_delta, scene_delta) = frontend
7538            .edit_segments(&mock_ctx, version, sketch_id, segments)
7539            .await
7540            .unwrap();
7541        assert_eq!(
7542            src_delta.text.as_str(),
7543            "\
7544sketch(on = XY) {
7545  line(start = [var 1mm, var 2mm], end = [var 127mm, var 152.4mm])
7546}
7547"
7548        );
7549        assert_eq!(scene_delta.new_objects, vec![]);
7550        assert_eq!(
7551            scene_delta.new_graph.objects.len(),
7552            5,
7553            "{:#?}",
7554            scene_delta.new_graph.objects
7555        );
7556
7557        ctx.close().await;
7558        mock_ctx.close().await;
7559    }
7560
7561    #[tokio::test(flavor = "multi_thread")]
7562    async fn test_edit_line_with_coincident_feedback() {
7563        let initial_source = "\
7564sketch(on = XY) {
7565  line1 = line(start = [var 1, var 2], end = [var 1, var 2])
7566  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
7567  fixed([line1.start, [0, 0]])
7568  coincident([line1.end, line2.start])
7569  equalLength([line1, line2])
7570}
7571";
7572
7573        let program = Program::parse(initial_source).unwrap().0.unwrap();
7574
7575        let mut frontend = FrontendState::new();
7576
7577        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7578        let mock_ctx = ExecutorContext::new_mock(None).await;
7579        let version = Version(0);
7580
7581        frontend.hack_set_program(&ctx, program).await.unwrap();
7582        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7583        let sketch_id = sketch_object.id;
7584        let sketch = expect_sketch(sketch_object);
7585        let line2_end_id = *sketch.segments.get(4).unwrap();
7586
7587        let segments = vec![ExistingSegmentCtor {
7588            id: line2_end_id,
7589            ctor: SegmentCtor::Point(PointCtor {
7590                position: Point2d {
7591                    x: Expr::Var(Number {
7592                        value: 9.0,
7593                        units: NumericSuffix::None,
7594                    }),
7595                    y: Expr::Var(Number {
7596                        value: 10.0,
7597                        units: NumericSuffix::None,
7598                    }),
7599                },
7600            }),
7601        }];
7602        let (src_delta, scene_delta) = frontend
7603            .edit_segments(&mock_ctx, version, sketch_id, segments)
7604            .await
7605            .unwrap();
7606        assert_eq!(
7607            src_delta.text.as_str(),
7608            "\
7609sketch(on = XY) {
7610  line1 = line(start = [var 0mm, var 0mm], end = [var 4.14mm, var 5.32mm])
7611  line2 = line(start = [var 4.14mm, var 5.32mm], end = [var 9mm, var 10mm])
7612  fixed([line1.start, [0, 0]])
7613  coincident([line1.end, line2.start])
7614  equalLength([line1, line2])
7615}
7616"
7617        );
7618        assert_eq!(
7619            scene_delta.new_graph.objects.len(),
7620            11,
7621            "{:#?}",
7622            scene_delta.new_graph.objects
7623        );
7624
7625        ctx.close().await;
7626        mock_ctx.close().await;
7627    }
7628
7629    #[tokio::test(flavor = "multi_thread")]
7630    async fn test_delete_point_without_var() {
7631        let initial_source = "\
7632sketch(on = XY) {
7633  point(at = [var 1, var 2])
7634  point(at = [var 3, var 4])
7635  point(at = [var 5, var 6])
7636}
7637";
7638
7639        let program = Program::parse(initial_source).unwrap().0.unwrap();
7640
7641        let mut frontend = FrontendState::new();
7642
7643        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7644        let mock_ctx = ExecutorContext::new_mock(None).await;
7645        let version = Version(0);
7646
7647        frontend.hack_set_program(&ctx, program).await.unwrap();
7648        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7649        let sketch_id = sketch_object.id;
7650        let sketch = expect_sketch(sketch_object);
7651
7652        let point_id = *sketch.segments.get(1).unwrap();
7653
7654        let (src_delta, scene_delta) = frontend
7655            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
7656            .await
7657            .unwrap();
7658        assert_eq!(
7659            src_delta.text.as_str(),
7660            "\
7661sketch(on = XY) {
7662  point(at = [var 1mm, var 2mm])
7663  point(at = [var 5mm, var 6mm])
7664}
7665"
7666        );
7667        assert_eq!(scene_delta.new_objects, vec![]);
7668        assert_eq!(scene_delta.new_graph.objects.len(), 4);
7669
7670        ctx.close().await;
7671        mock_ctx.close().await;
7672    }
7673
7674    #[tokio::test(flavor = "multi_thread")]
7675    async fn test_delete_point_with_var() {
7676        let initial_source = "\
7677sketch(on = XY) {
7678  point(at = [var 1, var 2])
7679  point1 = point(at = [var 3, var 4])
7680  point(at = [var 5, var 6])
7681}
7682";
7683
7684        let program = Program::parse(initial_source).unwrap().0.unwrap();
7685
7686        let mut frontend = FrontendState::new();
7687
7688        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7689        let mock_ctx = ExecutorContext::new_mock(None).await;
7690        let version = Version(0);
7691
7692        frontend.hack_set_program(&ctx, program).await.unwrap();
7693        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7694        let sketch_id = sketch_object.id;
7695        let sketch = expect_sketch(sketch_object);
7696
7697        let point_id = *sketch.segments.get(1).unwrap();
7698
7699        let (src_delta, scene_delta) = frontend
7700            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
7701            .await
7702            .unwrap();
7703        assert_eq!(
7704            src_delta.text.as_str(),
7705            "\
7706sketch(on = XY) {
7707  point(at = [var 1mm, var 2mm])
7708  point(at = [var 5mm, var 6mm])
7709}
7710"
7711        );
7712        assert_eq!(scene_delta.new_objects, vec![]);
7713        assert_eq!(scene_delta.new_graph.objects.len(), 4);
7714
7715        ctx.close().await;
7716        mock_ctx.close().await;
7717    }
7718
7719    #[tokio::test(flavor = "multi_thread")]
7720    async fn test_delete_multiple_points() {
7721        let initial_source = "\
7722sketch(on = XY) {
7723  point(at = [var 1, var 2])
7724  point1 = point(at = [var 3, var 4])
7725  point(at = [var 5, var 6])
7726}
7727";
7728
7729        let program = Program::parse(initial_source).unwrap().0.unwrap();
7730
7731        let mut frontend = FrontendState::new();
7732
7733        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7734        let mock_ctx = ExecutorContext::new_mock(None).await;
7735        let version = Version(0);
7736
7737        frontend.hack_set_program(&ctx, program).await.unwrap();
7738        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7739        let sketch_id = sketch_object.id;
7740
7741        let sketch = expect_sketch(sketch_object);
7742
7743        let point1_id = *sketch.segments.first().unwrap();
7744        let point2_id = *sketch.segments.get(1).unwrap();
7745
7746        let (src_delta, scene_delta) = frontend
7747            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point1_id, point2_id])
7748            .await
7749            .unwrap();
7750        assert_eq!(
7751            src_delta.text.as_str(),
7752            "\
7753sketch(on = XY) {
7754  point(at = [var 5mm, var 6mm])
7755}
7756"
7757        );
7758        assert_eq!(scene_delta.new_objects, vec![]);
7759        assert_eq!(scene_delta.new_graph.objects.len(), 3);
7760
7761        ctx.close().await;
7762        mock_ctx.close().await;
7763    }
7764
7765    #[tokio::test(flavor = "multi_thread")]
7766    async fn test_delete_coincident_constraint() {
7767        let initial_source = "\
7768sketch(on = XY) {
7769  point1 = point(at = [var 1, var 2])
7770  point2 = point(at = [var 3, var 4])
7771  coincident([point1, point2])
7772  point(at = [var 5, var 6])
7773}
7774";
7775
7776        let program = Program::parse(initial_source).unwrap().0.unwrap();
7777
7778        let mut frontend = FrontendState::new();
7779
7780        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7781        let mock_ctx = ExecutorContext::new_mock(None).await;
7782        let version = Version(0);
7783
7784        frontend.hack_set_program(&ctx, program).await.unwrap();
7785        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7786        let sketch_id = sketch_object.id;
7787        let sketch = expect_sketch(sketch_object);
7788
7789        let coincident_id = *sketch.constraints.first().unwrap();
7790
7791        let (src_delta, scene_delta) = frontend
7792            .delete_objects(&mock_ctx, version, sketch_id, vec![coincident_id], Vec::new())
7793            .await
7794            .unwrap();
7795        assert_eq!(
7796            src_delta.text.as_str(),
7797            "\
7798sketch(on = XY) {
7799  point1 = point(at = [var 1mm, var 2mm])
7800  point2 = point(at = [var 3mm, var 4mm])
7801  point(at = [var 5mm, var 6mm])
7802}
7803"
7804        );
7805        assert_eq!(scene_delta.new_objects, vec![]);
7806        assert_eq!(scene_delta.new_graph.objects.len(), 5);
7807
7808        ctx.close().await;
7809        mock_ctx.close().await;
7810    }
7811
7812    #[tokio::test(flavor = "multi_thread")]
7813    async fn test_delete_line_cascades_to_coincident_constraint() {
7814        let initial_source = "\
7815sketch(on = XY) {
7816  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
7817  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
7818  coincident([line1.end, line2.start])
7819}
7820";
7821
7822        let program = Program::parse(initial_source).unwrap().0.unwrap();
7823
7824        let mut frontend = FrontendState::new();
7825
7826        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7827        let mock_ctx = ExecutorContext::new_mock(None).await;
7828        let version = Version(0);
7829
7830        frontend.hack_set_program(&ctx, program).await.unwrap();
7831        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7832        let sketch_id = sketch_object.id;
7833        let sketch = expect_sketch(sketch_object);
7834        let line_id = *sketch.segments.get(5).unwrap();
7835
7836        let (src_delta, scene_delta) = frontend
7837            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
7838            .await
7839            .unwrap();
7840        assert_eq!(
7841            src_delta.text.as_str(),
7842            "\
7843sketch(on = XY) {
7844  line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
7845}
7846"
7847        );
7848        assert_eq!(
7849            scene_delta.new_graph.objects.len(),
7850            5,
7851            "{:#?}",
7852            scene_delta.new_graph.objects
7853        );
7854
7855        ctx.close().await;
7856        mock_ctx.close().await;
7857    }
7858
7859    #[tokio::test(flavor = "multi_thread")]
7860    async fn test_delete_line_cascades_to_distance_constraint() {
7861        let initial_source = "\
7862sketch(on = XY) {
7863  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
7864  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
7865  distance([line1.end, line2.start]) == 10mm
7866}
7867";
7868
7869        let program = Program::parse(initial_source).unwrap().0.unwrap();
7870
7871        let mut frontend = FrontendState::new();
7872
7873        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7874        let mock_ctx = ExecutorContext::new_mock(None).await;
7875        let version = Version(0);
7876
7877        frontend.hack_set_program(&ctx, program).await.unwrap();
7878        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7879        let sketch_id = sketch_object.id;
7880        let sketch = expect_sketch(sketch_object);
7881        let line_id = *sketch.segments.get(5).unwrap();
7882
7883        let (src_delta, scene_delta) = frontend
7884            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
7885            .await
7886            .unwrap();
7887        assert_eq!(
7888            src_delta.text.as_str(),
7889            "\
7890sketch(on = XY) {
7891  line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
7892}
7893"
7894        );
7895        assert_eq!(
7896            scene_delta.new_graph.objects.len(),
7897            5,
7898            "{:#?}",
7899            scene_delta.new_graph.objects
7900        );
7901
7902        ctx.close().await;
7903        mock_ctx.close().await;
7904    }
7905
7906    #[tokio::test(flavor = "multi_thread")]
7907    async fn test_delete_point_cascades_to_horizontal_distance_constraint() {
7908        let initial_source = "\
7909sketch(on = XY) {
7910  point1 = point(at = [var 1, var 2])
7911  point2 = point(at = [var 3, var 4])
7912  horizontalDistance([point1, point2]) == 10mm
7913}
7914";
7915
7916        let program = Program::parse(initial_source).unwrap().0.unwrap();
7917
7918        let mut frontend = FrontendState::new();
7919
7920        let mock_ctx = ExecutorContext::new_mock(None).await;
7921        let version = Version(0);
7922
7923        frontend.program = program.clone();
7924        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
7925        frontend.update_state_after_exec(outcome, true);
7926        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7927        let sketch_id = sketch_object.id;
7928        let sketch = expect_sketch(sketch_object);
7929        let point2_id = *sketch.segments.get(1).unwrap();
7930
7931        let (src_delta, scene_delta) = frontend
7932            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point2_id])
7933            .await
7934            .unwrap();
7935        assert_eq!(
7936            src_delta.text.as_str(),
7937            "\
7938sketch(on = XY) {
7939  point1 = point(at = [var 1mm, var 2mm])
7940}
7941"
7942        );
7943        assert_eq!(
7944            scene_delta.new_graph.objects.len(),
7945            3,
7946            "{:#?}",
7947            scene_delta.new_graph.objects
7948        );
7949
7950        mock_ctx.close().await;
7951    }
7952
7953    #[tokio::test(flavor = "multi_thread")]
7954    async fn test_delete_line_cascades_to_fixed_constraint() {
7955        let initial_source = "\
7956sketch(on = XY) {
7957  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
7958  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
7959  fixed([line1.start, [0, 0]])
7960}
7961";
7962
7963        let program = Program::parse(initial_source).unwrap().0.unwrap();
7964
7965        let mut frontend = FrontendState::new();
7966
7967        let mock_ctx = ExecutorContext::new_mock(None).await;
7968        let version = Version(0);
7969
7970        frontend.program = program.clone();
7971        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
7972        frontend.update_state_after_exec(outcome, true);
7973        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7974        let sketch_id = sketch_object.id;
7975        let sketch = expect_sketch(sketch_object);
7976        let line1_id = *sketch.segments.get(2).unwrap();
7977
7978        let (src_delta, scene_delta) = frontend
7979            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
7980            .await
7981            .unwrap();
7982        assert_eq!(
7983            src_delta.text.as_str(),
7984            "\
7985sketch(on = XY) {
7986  line2 = line(start = [var 5mm, var 6mm], end = [var 7mm, var 8mm])
7987}
7988"
7989        );
7990        assert_eq!(
7991            scene_delta.new_graph.objects.len(),
7992            5,
7993            "{:#?}",
7994            scene_delta.new_graph.objects
7995        );
7996
7997        mock_ctx.close().await;
7998    }
7999
8000    #[tokio::test(flavor = "multi_thread")]
8001    async fn test_delete_line_cascades_to_midpoint_constraint() {
8002        let initial_source = "\
8003sketch(on = XY) {
8004  point1 = point(at = [var 1, var 2])
8005  line1 = line(start = [var 0, var 0], end = [var 6, var 4])
8006  midpoint(line1, point = point1)
8007}
8008";
8009
8010        let program = Program::parse(initial_source).unwrap().0.unwrap();
8011
8012        let mut frontend = FrontendState::new();
8013
8014        let mock_ctx = ExecutorContext::new_mock(None).await;
8015        let version = Version(0);
8016
8017        frontend.program = program.clone();
8018        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
8019        frontend.update_state_after_exec(outcome, true);
8020        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8021        let sketch_id = sketch_object.id;
8022        let sketch = expect_sketch(sketch_object);
8023        let line1_id = *sketch.segments.get(3).unwrap();
8024
8025        let (src_delta, scene_delta) = frontend
8026            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
8027            .await
8028            .unwrap();
8029        assert_eq!(
8030            src_delta.text.as_str(),
8031            "\
8032sketch(on = XY) {
8033  point1 = point(at = [var 1mm, var 2mm])
8034}
8035"
8036        );
8037        assert_eq!(
8038            scene_delta.new_graph.objects.len(),
8039            3,
8040            "{:#?}",
8041            scene_delta.new_graph.objects
8042        );
8043
8044        mock_ctx.close().await;
8045    }
8046
8047    #[tokio::test(flavor = "multi_thread")]
8048    async fn test_delete_point_preserves_multiline_coincident_constraint() {
8049        let initial_source = "\
8050sketch(on = XY) {
8051  point1 = point(at = [var 1, var 2])
8052  point2 = point(at = [var 3, var 4])
8053  point3 = point(at = [var 5, var 6])
8054  coincident([point1, point2, point3])
8055}
8056";
8057
8058        let program = Program::parse(initial_source).unwrap().0.unwrap();
8059
8060        let mut frontend = FrontendState::new();
8061
8062        let mock_ctx = ExecutorContext::new_mock(None).await;
8063        let version = Version(0);
8064
8065        frontend.program = program.clone();
8066        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
8067        frontend.update_state_after_exec(outcome, true);
8068        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8069        let sketch_id = sketch_object.id;
8070        let sketch = expect_sketch(sketch_object);
8071        let point3_id = *sketch.segments.get(2).unwrap();
8072
8073        let (src_delta, scene_delta) = frontend
8074            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point3_id])
8075            .await
8076            .unwrap();
8077        assert!(src_delta.text.contains("point1 = point("), "{}", src_delta.text);
8078        assert!(src_delta.text.contains("point2 = point("), "{}", src_delta.text);
8079        assert!(!src_delta.text.contains("point3 = point("), "{}", src_delta.text);
8080        assert!(
8081            src_delta.text.contains("coincident([point1, point2])"),
8082            "{}",
8083            src_delta.text
8084        );
8085
8086        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8087        let sketch = expect_sketch(sketch_object);
8088        assert_eq!(sketch.segments.len(), 2);
8089        assert_eq!(sketch.constraints.len(), 1);
8090
8091        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
8092        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
8093            panic!("Expected constraint object");
8094        };
8095        let Constraint::Coincident(coincident) = constraint else {
8096            panic!("Expected coincident constraint");
8097        };
8098        assert_eq!(
8099            coincident.segments,
8100            sketch
8101                .segments
8102                .iter()
8103                .copied()
8104                .map(Into::into)
8105                .collect::<Vec<ConstraintSegment>>()
8106        );
8107
8108        mock_ctx.close().await;
8109    }
8110
8111    #[tokio::test(flavor = "multi_thread")]
8112    async fn test_delete_line_preserves_multiline_equal_length_constraint() {
8113        let initial_source = "\
8114sketch(on = XY) {
8115  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8116  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8117  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
8118  equalLength([line1, line2, line3])
8119}
8120";
8121
8122        let program = Program::parse(initial_source).unwrap().0.unwrap();
8123
8124        let mut frontend = FrontendState::new();
8125
8126        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8127        let mock_ctx = ExecutorContext::new_mock(None).await;
8128        let version = Version(0);
8129
8130        frontend.hack_set_program(&ctx, program).await.unwrap();
8131        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8132        let sketch_id = sketch_object.id;
8133        let sketch = expect_sketch(sketch_object);
8134        let line3_id = *sketch.segments.get(8).unwrap();
8135
8136        let (src_delta, scene_delta) = frontend
8137            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line3_id])
8138            .await
8139            .unwrap();
8140        assert_eq!(
8141            src_delta.text.as_str(),
8142            "\
8143sketch(on = XY) {
8144  line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
8145  line2 = line(start = [var 5mm, var 6mm], end = [var 7mm, var 8mm])
8146  equalLength([line1, line2])
8147}
8148"
8149        );
8150
8151        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8152        let sketch = expect_sketch(sketch_object);
8153        assert_eq!(sketch.constraints.len(), 1);
8154
8155        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
8156        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
8157            panic!("Expected constraint object");
8158        };
8159        let Constraint::LinesEqualLength(lines_equal_length) = constraint else {
8160            panic!("Expected lines equal length constraint");
8161        };
8162        assert_eq!(lines_equal_length.lines.len(), 2);
8163
8164        ctx.close().await;
8165        mock_ctx.close().await;
8166    }
8167
8168    #[tokio::test(flavor = "multi_thread")]
8169    async fn test_delete_line_preserves_multiline_horizontal_constraint() {
8170        let initial_source = "\
8171sketch(on = XY) {
8172  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8173  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8174  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
8175  horizontal([line1.end, line2.start, line3.start])
8176}
8177";
8178
8179        let program = Program::parse(initial_source).unwrap().0.unwrap();
8180
8181        let mut frontend = FrontendState::new();
8182
8183        let mock_ctx = ExecutorContext::new_mock(None).await;
8184        let version = Version(0);
8185
8186        frontend.program = program.clone();
8187        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
8188        frontend.update_state_after_exec(outcome, true);
8189        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8190        let sketch_id = sketch_object.id;
8191        let sketch = expect_sketch(sketch_object);
8192        let line1_id = *sketch.segments.get(2).unwrap();
8193
8194        let (src_delta, scene_delta) = frontend
8195            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
8196            .await
8197            .unwrap();
8198        assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
8199        assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
8200        assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
8201        assert!(
8202            src_delta.text.contains("horizontal([line2.start, line3.start])"),
8203            "{}",
8204            src_delta.text
8205        );
8206
8207        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8208        let sketch = expect_sketch(sketch_object);
8209        assert_eq!(sketch.constraints.len(), 1);
8210
8211        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
8212        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
8213            panic!("Expected constraint object");
8214        };
8215        let Constraint::Horizontal(Horizontal::Points { points }) = constraint else {
8216            panic!("Expected horizontal points constraint");
8217        };
8218        let remaining_points = vec![sketch.segments[0].into(), sketch.segments[3].into()];
8219        assert_eq!(*points, remaining_points);
8220
8221        mock_ctx.close().await;
8222    }
8223
8224    #[tokio::test(flavor = "multi_thread")]
8225    async fn test_delete_line_preserves_multiline_vertical_constraint() {
8226        let initial_source = "\
8227sketch(on = XY) {
8228  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8229  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8230  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
8231  vertical([line1.end, line2.start, line3.start])
8232}
8233";
8234
8235        let program = Program::parse(initial_source).unwrap().0.unwrap();
8236
8237        let mut frontend = FrontendState::new();
8238
8239        let mock_ctx = ExecutorContext::new_mock(None).await;
8240        let version = Version(0);
8241
8242        frontend.program = program.clone();
8243        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
8244        frontend.update_state_after_exec(outcome, true);
8245        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8246        let sketch_id = sketch_object.id;
8247        let sketch = expect_sketch(sketch_object);
8248        let line1_id = *sketch.segments.get(2).unwrap();
8249
8250        let (src_delta, scene_delta) = frontend
8251            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
8252            .await
8253            .unwrap();
8254        assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
8255        assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
8256        assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
8257        assert!(
8258            src_delta.text.contains("vertical([line2.start, line3.start])"),
8259            "{}",
8260            src_delta.text
8261        );
8262
8263        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8264        let sketch = expect_sketch(sketch_object);
8265        assert_eq!(sketch.constraints.len(), 1);
8266
8267        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
8268        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
8269            panic!("Expected constraint object");
8270        };
8271        let Constraint::Vertical(Vertical::Points { points }) = constraint else {
8272            panic!("Expected vertical points constraint");
8273        };
8274        let remaining_points = vec![sketch.segments[0].into(), sketch.segments[3].into()];
8275        assert_eq!(*points, remaining_points);
8276
8277        mock_ctx.close().await;
8278    }
8279
8280    #[tokio::test(flavor = "multi_thread")]
8281    async fn test_delete_line_preserves_multiline_coincident_constraint() {
8282        let initial_source = "\
8283sketch(on = XY) {
8284  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8285  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8286  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
8287  coincident([line1.end, line2.start, line3.start])
8288}
8289";
8290
8291        let program = Program::parse(initial_source).unwrap().0.unwrap();
8292
8293        let mut frontend = FrontendState::new();
8294
8295        let mock_ctx = ExecutorContext::new_mock(None).await;
8296        let version = Version(0);
8297
8298        frontend.program = program.clone();
8299        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
8300        frontend.update_state_after_exec(outcome, true);
8301        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8302        let sketch_id = sketch_object.id;
8303        let sketch = expect_sketch(sketch_object);
8304        let line1_id = *sketch.segments.get(2).unwrap();
8305
8306        let (src_delta, scene_delta) = frontend
8307            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
8308            .await
8309            .unwrap();
8310        assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
8311        assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
8312        assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
8313        assert!(
8314            src_delta.text.contains("coincident([line2.start, line3.start])"),
8315            "{}",
8316            src_delta.text
8317        );
8318
8319        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8320        let sketch = expect_sketch(sketch_object);
8321        assert_eq!(sketch.constraints.len(), 1);
8322
8323        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
8324        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
8325            panic!("Expected constraint object");
8326        };
8327        let Constraint::Coincident(coincident) = constraint else {
8328            panic!("Expected coincident constraint");
8329        };
8330        let remaining_segments = vec![sketch.segments[0].into(), sketch.segments[3].into()];
8331        assert_eq!(coincident.segments, remaining_segments);
8332
8333        mock_ctx.close().await;
8334    }
8335
8336    #[tokio::test(flavor = "multi_thread")]
8337    async fn test_delete_lines_removes_multiline_equal_length_constraint_below_minimum() {
8338        let initial_source = "\
8339sketch(on = XY) {
8340  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8341  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8342  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
8343  equalLength([line1, line2, line3])
8344}
8345";
8346
8347        let program = Program::parse(initial_source).unwrap().0.unwrap();
8348
8349        let mut frontend = FrontendState::new();
8350
8351        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8352        let mock_ctx = ExecutorContext::new_mock(None).await;
8353        let version = Version(0);
8354
8355        frontend.hack_set_program(&ctx, program).await.unwrap();
8356        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8357        let sketch_id = sketch_object.id;
8358        let sketch = expect_sketch(sketch_object);
8359        let line2_id = *sketch.segments.get(5).unwrap();
8360        let line3_id = *sketch.segments.get(8).unwrap();
8361
8362        let (src_delta, scene_delta) = frontend
8363            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line2_id, line3_id])
8364            .await
8365            .unwrap();
8366        assert_eq!(
8367            src_delta.text.as_str(),
8368            "\
8369sketch(on = XY) {
8370  line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
8371}
8372"
8373        );
8374
8375        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8376        let sketch = expect_sketch(sketch_object);
8377        assert!(sketch.constraints.is_empty());
8378
8379        ctx.close().await;
8380        mock_ctx.close().await;
8381    }
8382
8383    #[tokio::test(flavor = "multi_thread")]
8384    async fn test_delete_line_preserves_multiline_parallel_constraint() {
8385        let initial_source = "\
8386sketch(on = XY) {
8387  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8388  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8389  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
8390  parallel([line1, line2, line3])
8391}
8392";
8393
8394        let program = Program::parse(initial_source).unwrap().0.unwrap();
8395
8396        let mut frontend = FrontendState::new();
8397
8398        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8399        let mock_ctx = ExecutorContext::new_mock(None).await;
8400        let version = Version(0);
8401
8402        frontend.hack_set_program(&ctx, program).await.unwrap();
8403        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8404        let sketch_id = sketch_object.id;
8405        let sketch = expect_sketch(sketch_object);
8406        let line3_id = *sketch.segments.get(8).unwrap();
8407
8408        let (src_delta, scene_delta) = frontend
8409            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line3_id])
8410            .await
8411            .unwrap();
8412        assert_eq!(
8413            src_delta.text.as_str(),
8414            "\
8415sketch(on = XY) {
8416  line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
8417  line2 = line(start = [var 5mm, var 6mm], end = [var 7mm, var 8mm])
8418  parallel([line1, line2])
8419}
8420"
8421        );
8422
8423        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8424        let sketch = expect_sketch(sketch_object);
8425        assert_eq!(sketch.constraints.len(), 1);
8426
8427        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
8428        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
8429            panic!("Expected constraint object");
8430        };
8431        let Constraint::Parallel(parallel) = constraint else {
8432            panic!("Expected parallel constraint");
8433        };
8434        assert_eq!(parallel.lines.len(), 2);
8435
8436        ctx.close().await;
8437        mock_ctx.close().await;
8438    }
8439
8440    #[tokio::test(flavor = "multi_thread")]
8441    async fn test_delete_lines_removes_multiline_parallel_constraint_below_minimum() {
8442        let initial_source = "\
8443sketch(on = XY) {
8444  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8445  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8446  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
8447  parallel([line1, line2, line3])
8448}
8449";
8450
8451        let program = Program::parse(initial_source).unwrap().0.unwrap();
8452
8453        let mut frontend = FrontendState::new();
8454
8455        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8456        let mock_ctx = ExecutorContext::new_mock(None).await;
8457        let version = Version(0);
8458
8459        frontend.hack_set_program(&ctx, program).await.unwrap();
8460        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8461        let sketch_id = sketch_object.id;
8462        let sketch = expect_sketch(sketch_object);
8463        let line2_id = *sketch.segments.get(5).unwrap();
8464        let line3_id = *sketch.segments.get(8).unwrap();
8465
8466        let (src_delta, scene_delta) = frontend
8467            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line2_id, line3_id])
8468            .await
8469            .unwrap();
8470        assert_eq!(
8471            src_delta.text.as_str(),
8472            "\
8473sketch(on = XY) {
8474  line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
8475}
8476"
8477        );
8478
8479        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8480        let sketch = expect_sketch(sketch_object);
8481        assert!(sketch.constraints.is_empty());
8482
8483        ctx.close().await;
8484        mock_ctx.close().await;
8485    }
8486
8487    #[tokio::test(flavor = "multi_thread")]
8488    async fn test_delete_line_line_coincident_constraint() {
8489        let initial_source = "\
8490sketch(on = XY) {
8491  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8492  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8493  coincident([line1, line2])
8494}
8495";
8496
8497        let program = Program::parse(initial_source).unwrap().0.unwrap();
8498
8499        let mut frontend = FrontendState::new();
8500
8501        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8502        let mock_ctx = ExecutorContext::new_mock(None).await;
8503        let version = Version(0);
8504
8505        frontend.hack_set_program(&ctx, program).await.unwrap();
8506        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8507        let sketch_id = sketch_object.id;
8508        let sketch = expect_sketch(sketch_object);
8509
8510        let coincident_id = *sketch.constraints.first().unwrap();
8511
8512        let (src_delta, scene_delta) = frontend
8513            .delete_objects(&mock_ctx, version, sketch_id, vec![coincident_id], Vec::new())
8514            .await
8515            .unwrap();
8516        assert_eq!(
8517            src_delta.text.as_str(),
8518            "\
8519sketch(on = XY) {
8520  line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
8521  line2 = line(start = [var 5mm, var 6mm], end = [var 7mm, var 8mm])
8522}
8523"
8524        );
8525        assert_eq!(scene_delta.new_objects, vec![]);
8526        assert_eq!(scene_delta.new_graph.objects.len(), 8);
8527
8528        ctx.close().await;
8529        mock_ctx.close().await;
8530    }
8531
8532    #[tokio::test(flavor = "multi_thread")]
8533    async fn test_two_points_coincident() {
8534        let initial_source = "\
8535sketch(on = XY) {
8536  point1 = point(at = [var 1, var 2])
8537  point(at = [3, 4])
8538}
8539";
8540
8541        let program = Program::parse(initial_source).unwrap().0.unwrap();
8542
8543        let mut frontend = FrontendState::new();
8544
8545        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8546        let mock_ctx = ExecutorContext::new_mock(None).await;
8547        let version = Version(0);
8548
8549        frontend.hack_set_program(&ctx, program).await.unwrap();
8550        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8551        let sketch_id = sketch_object.id;
8552        let sketch = expect_sketch(sketch_object);
8553        let point0_id = *sketch.segments.first().unwrap();
8554        let point1_id = *sketch.segments.get(1).unwrap();
8555
8556        let constraint = Constraint::Coincident(Coincident {
8557            segments: vec![point0_id.into(), point1_id.into()],
8558        });
8559        let (src_delta, scene_delta) = frontend
8560            .add_constraint(&mock_ctx, version, sketch_id, constraint)
8561            .await
8562            .unwrap();
8563        assert_eq!(
8564            src_delta.text.as_str(),
8565            "\
8566sketch(on = XY) {
8567  point1 = point(at = [var 1, var 2])
8568  point2 = point(at = [3, 4])
8569  coincident([point1, point2])
8570}
8571"
8572        );
8573        assert_eq!(
8574            scene_delta.new_graph.objects.len(),
8575            5,
8576            "{:#?}",
8577            scene_delta.new_graph.objects
8578        );
8579
8580        ctx.close().await;
8581        mock_ctx.close().await;
8582    }
8583
8584    #[tokio::test(flavor = "multi_thread")]
8585    async fn test_three_points_coincident() {
8586        let initial_source = "\
8587sketch(on = XY) {
8588  point1 = point(at = [var 1, var 2])
8589  point(at = [var 3, var 4])
8590  point(at = [var 5, var 6])
8591}
8592";
8593
8594        let program = Program::parse(initial_source).unwrap().0.unwrap();
8595
8596        let mut frontend = FrontendState::new();
8597
8598        let mock_ctx = ExecutorContext::new_mock(None).await;
8599        let version = Version(0);
8600
8601        frontend.program = program.clone();
8602        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
8603        frontend.update_state_after_exec(outcome, true);
8604        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8605        let sketch_id = sketch_object.id;
8606        let sketch = expect_sketch(sketch_object);
8607        let segments = sketch
8608            .segments
8609            .iter()
8610            .take(3)
8611            .copied()
8612            .map(Into::into)
8613            .collect::<Vec<ConstraintSegment>>();
8614
8615        let constraint = Constraint::Coincident(Coincident {
8616            segments: segments.clone(),
8617        });
8618        let (src_delta, scene_delta) = frontend
8619            .add_constraint(&mock_ctx, version, sketch_id, constraint)
8620            .await
8621            .unwrap();
8622        assert_eq!(
8623            src_delta.text.as_str(),
8624            "\
8625sketch(on = XY) {
8626  point1 = point(at = [var 1, var 2])
8627  point2 = point(at = [var 3, var 4])
8628  point3 = point(at = [var 5, var 6])
8629  coincident([point1, point2, point3])
8630}
8631"
8632        );
8633
8634        let constraint_object = scene_delta
8635            .new_graph
8636            .objects
8637            .iter()
8638            .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
8639            .unwrap();
8640
8641        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
8642            panic!("expected a constraint object");
8643        };
8644
8645        assert_eq!(constraint, &Constraint::Coincident(Coincident { segments }));
8646
8647        mock_ctx.close().await;
8648    }
8649
8650    #[tokio::test(flavor = "multi_thread")]
8651    async fn test_source_with_three_point_coincident_tracks_all_segments() {
8652        let initial_source = "\
8653sketch(on = XY) {
8654  point1 = point(at = [var 1, var 2])
8655  point2 = point(at = [var 3, var 4])
8656  point3 = point(at = [var 5, var 6])
8657  coincident([point1, point2, point3])
8658}
8659";
8660
8661        let program = Program::parse(initial_source).unwrap().0.unwrap();
8662
8663        let mut frontend = FrontendState::new();
8664
8665        let ctx = ExecutorContext::new_mock(None).await;
8666        frontend.program = program.clone();
8667        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
8668        frontend.update_state_after_exec(outcome, true);
8669
8670        let constraint_object = frontend
8671            .scene_graph
8672            .objects
8673            .iter()
8674            .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
8675            .unwrap();
8676        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
8677            panic!("expected a constraint object");
8678        };
8679
8680        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8681        let sketch = expect_sketch(sketch_object);
8682        let expected_segments = sketch
8683            .segments
8684            .iter()
8685            .take(3)
8686            .copied()
8687            .map(Into::into)
8688            .collect::<Vec<ConstraintSegment>>();
8689
8690        assert_eq!(
8691            constraint,
8692            &Constraint::Coincident(Coincident {
8693                segments: expected_segments,
8694            })
8695        );
8696
8697        ctx.close().await;
8698    }
8699
8700    #[tokio::test(flavor = "multi_thread")]
8701    async fn test_point_origin_coincident_preserves_order() {
8702        let initial_source = "\
8703sketch(on = XY) {
8704  point(at = [var 1, var 2])
8705}
8706";
8707
8708        for (origin_first, expected_source) in [
8709            (
8710                true,
8711                "\
8712sketch(on = XY) {
8713  point1 = point(at = [var 1, var 2])
8714  coincident([ORIGIN, point1])
8715}
8716",
8717            ),
8718            (
8719                false,
8720                "\
8721sketch(on = XY) {
8722  point1 = point(at = [var 1, var 2])
8723  coincident([point1, ORIGIN])
8724}
8725",
8726            ),
8727        ] {
8728            let program = Program::parse(initial_source).unwrap().0.unwrap();
8729
8730            let mut frontend = FrontendState::new();
8731
8732            let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8733            let mock_ctx = ExecutorContext::new_mock(None).await;
8734            let version = Version(0);
8735
8736            frontend.hack_set_program(&ctx, program).await.unwrap();
8737            let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8738            let sketch_id = sketch_object.id;
8739            let sketch = expect_sketch(sketch_object);
8740            let point_id = *sketch.segments.first().unwrap();
8741
8742            let segments = if origin_first {
8743                vec![ConstraintSegment::ORIGIN, point_id.into()]
8744            } else {
8745                vec![point_id.into(), ConstraintSegment::ORIGIN]
8746            };
8747            let constraint = Constraint::Coincident(Coincident {
8748                segments: segments.clone(),
8749            });
8750            let (src_delta, scene_delta) = frontend
8751                .add_constraint(&mock_ctx, version, sketch_id, constraint)
8752                .await
8753                .unwrap();
8754            assert_eq!(src_delta.text.as_str(), expected_source);
8755
8756            let constraint_object = scene_delta
8757                .new_graph
8758                .objects
8759                .iter()
8760                .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
8761                .unwrap();
8762
8763            let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
8764                panic!("expected a constraint object");
8765            };
8766
8767            assert_eq!(constraint, &Constraint::Coincident(Coincident { segments }));
8768
8769            ctx.close().await;
8770            mock_ctx.close().await;
8771        }
8772    }
8773
8774    #[tokio::test(flavor = "multi_thread")]
8775    async fn test_coincident_of_line_end_points() {
8776        let initial_source = "\
8777sketch(on = XY) {
8778  line(start = [var 1, var 2], end = [var 3, var 4])
8779  line(start = [var 5, var 6], end = [var 7, var 8])
8780}
8781";
8782
8783        let program = Program::parse(initial_source).unwrap().0.unwrap();
8784
8785        let mut frontend = FrontendState::new();
8786
8787        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8788        let mock_ctx = ExecutorContext::new_mock(None).await;
8789        let version = Version(0);
8790
8791        frontend.hack_set_program(&ctx, program).await.unwrap();
8792        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8793        let sketch_id = sketch_object.id;
8794        let sketch = expect_sketch(sketch_object);
8795        let point0_id = *sketch.segments.get(1).unwrap();
8796        let point1_id = *sketch.segments.get(3).unwrap();
8797
8798        let constraint = Constraint::Coincident(Coincident {
8799            segments: vec![point0_id.into(), point1_id.into()],
8800        });
8801        let (src_delta, scene_delta) = frontend
8802            .add_constraint(&mock_ctx, version, sketch_id, constraint)
8803            .await
8804            .unwrap();
8805        assert_eq!(
8806            src_delta.text.as_str(),
8807            "\
8808sketch(on = XY) {
8809  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8810  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8811  coincident([line1.end, line2.start])
8812}
8813"
8814        );
8815        assert_eq!(
8816            scene_delta.new_graph.objects.len(),
8817            9,
8818            "{:#?}",
8819            scene_delta.new_graph.objects
8820        );
8821
8822        ctx.close().await;
8823        mock_ctx.close().await;
8824    }
8825
8826    #[tokio::test(flavor = "multi_thread")]
8827    async fn test_coincident_of_line_point_and_circle_segment() {
8828        let initial_source = "\
8829sketch(on = XY) {
8830  circle1 = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
8831  line1 = line(start = [var 9mm, var 1mm], end = [var 10mm, var 2mm])
8832}
8833";
8834        let program = Program::parse(initial_source).unwrap().0.unwrap();
8835        let mut frontend = FrontendState::new();
8836
8837        let mock_ctx = ExecutorContext::new_mock(None).await;
8838        let version = Version(0);
8839
8840        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
8841        frontend.program = program;
8842        frontend.update_state_after_exec(outcome, true);
8843        let sketch_object = find_first_sketch_object(&frontend.scene_graph).expect("Expected sketch object");
8844        let sketch_id = sketch_object.id;
8845        let sketch = expect_sketch(sketch_object);
8846
8847        let circle_id = sketch
8848            .segments
8849            .iter()
8850            .copied()
8851            .find(|seg_id| {
8852                matches!(
8853                    &frontend.scene_graph.objects[seg_id.0].kind,
8854                    ObjectKind::Segment {
8855                        segment: Segment::Circle(_)
8856                    }
8857                )
8858            })
8859            .expect("Expected a circle segment in sketch");
8860        let line_id = sketch
8861            .segments
8862            .iter()
8863            .copied()
8864            .find(|seg_id| {
8865                matches!(
8866                    &frontend.scene_graph.objects[seg_id.0].kind,
8867                    ObjectKind::Segment {
8868                        segment: Segment::Line(_)
8869                    }
8870                )
8871            })
8872            .expect("Expected a line segment in sketch");
8873
8874        let line_start_point_id = match &frontend.scene_graph.objects[line_id.0].kind {
8875            ObjectKind::Segment {
8876                segment: Segment::Line(line),
8877            } => line.start,
8878            _ => panic!("Expected line segment object"),
8879        };
8880
8881        let constraint = Constraint::Coincident(Coincident {
8882            segments: vec![line_start_point_id.into(), circle_id.into()],
8883        });
8884        let (src_delta, _scene_delta) = frontend
8885            .add_constraint(&mock_ctx, version, sketch_id, constraint)
8886            .await
8887            .unwrap();
8888        assert_eq!(
8889            src_delta.text.as_str(),
8890            "\
8891sketch(on = XY) {
8892  circle1 = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
8893  line1 = line(start = [var 9mm, var 1mm], end = [var 10mm, var 2mm])
8894  coincident([line1.start, circle1])
8895}
8896"
8897        );
8898
8899        mock_ctx.close().await;
8900    }
8901
8902    #[tokio::test(flavor = "multi_thread")]
8903    async fn test_invalid_coincident_arc_and_line_preserves_state() {
8904        // Test that attempting an invalid coincident constraint (arc and line)
8905        // doesn't corrupt the state, allowing subsequent operations to work.
8906        // This test verifies the transactional fix in add_constraint that prevents
8907        // state corruption when invalid constraints are attempted.
8908        // Example: coincident constraint between an arc segment and a straight line segment
8909        // is geometrically invalid and should fail, but state should remain intact.
8910        // Use the programmatic approach (new_sketch + add_segment) like test_new_sketch_add_arc_edit_arc
8911        let program = Program::empty();
8912
8913        let mut frontend = FrontendState::new();
8914        frontend.program = program;
8915
8916        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8917        let mock_ctx = ExecutorContext::new_mock(None).await;
8918        let version = Version(0);
8919
8920        let sketch_args = SketchCtor {
8921            on: Plane::Default(PlaneName::Xy),
8922        };
8923        let (_src_delta, _scene_delta, sketch_id) = frontend
8924            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
8925            .await
8926            .unwrap();
8927
8928        // Add an arc segment
8929        let arc_ctor = ArcCtor {
8930            start: Point2d {
8931                x: Expr::Var(Number {
8932                    value: 0.0,
8933                    units: NumericSuffix::Mm,
8934                }),
8935                y: Expr::Var(Number {
8936                    value: 0.0,
8937                    units: NumericSuffix::Mm,
8938                }),
8939            },
8940            end: Point2d {
8941                x: Expr::Var(Number {
8942                    value: 10.0,
8943                    units: NumericSuffix::Mm,
8944                }),
8945                y: Expr::Var(Number {
8946                    value: 10.0,
8947                    units: NumericSuffix::Mm,
8948                }),
8949            },
8950            center: Point2d {
8951                x: Expr::Var(Number {
8952                    value: 10.0,
8953                    units: NumericSuffix::Mm,
8954                }),
8955                y: Expr::Var(Number {
8956                    value: 0.0,
8957                    units: NumericSuffix::Mm,
8958                }),
8959            },
8960            construction: None,
8961        };
8962        let (_src_delta, scene_delta) = frontend
8963            .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Arc(arc_ctor), None)
8964            .await
8965            .unwrap();
8966        // The arc is the last object in new_objects (after the 3 points: start, end, center)
8967        let arc_id = *scene_delta.new_objects.last().unwrap();
8968
8969        // Add a line segment
8970        let line_ctor = LineCtor {
8971            start: Point2d {
8972                x: Expr::Var(Number {
8973                    value: 20.0,
8974                    units: NumericSuffix::Mm,
8975                }),
8976                y: Expr::Var(Number {
8977                    value: 0.0,
8978                    units: NumericSuffix::Mm,
8979                }),
8980            },
8981            end: Point2d {
8982                x: Expr::Var(Number {
8983                    value: 30.0,
8984                    units: NumericSuffix::Mm,
8985                }),
8986                y: Expr::Var(Number {
8987                    value: 10.0,
8988                    units: NumericSuffix::Mm,
8989                }),
8990            },
8991            construction: None,
8992        };
8993        let (_src_delta, scene_delta) = frontend
8994            .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Line(line_ctor), None)
8995            .await
8996            .unwrap();
8997        // The line is the last object in new_objects (after the 2 points: start, end)
8998        let line_id = *scene_delta.new_objects.last().unwrap();
8999
9000        // Attempt to add an invalid coincident constraint between arc and line
9001        // This should fail during execution, but state should remain intact
9002        let constraint = Constraint::Coincident(Coincident {
9003            segments: vec![arc_id.into(), line_id.into()],
9004        });
9005        let result = frontend.add_constraint(&mock_ctx, version, sketch_id, constraint).await;
9006
9007        // The constraint addition should fail (invalid constraint)
9008        assert!(result.is_err(), "Expected invalid coincident constraint to fail");
9009
9010        // Verify state is not corrupted by checking that we can still access the scene graph
9011        // and that the original segments are still present with their source ranges
9012        let sketch_object_after =
9013            find_first_sketch_object(&frontend.scene_graph).expect("Sketch should still exist after failed constraint");
9014        let sketch_after = expect_sketch(sketch_object_after);
9015
9016        // Verify both segments are still in the sketch
9017        assert!(
9018            sketch_after.segments.contains(&arc_id),
9019            "Arc segment should still exist after failed constraint"
9020        );
9021        assert!(
9022            sketch_after.segments.contains(&line_id),
9023            "Line segment should still exist after failed constraint"
9024        );
9025
9026        // Verify we can still access segment objects (this would fail if source ranges were corrupted)
9027        let arc_obj = frontend
9028            .scene_graph
9029            .objects
9030            .get(arc_id.0)
9031            .expect("Arc object should still be accessible");
9032        let line_obj = frontend
9033            .scene_graph
9034            .objects
9035            .get(line_id.0)
9036            .expect("Line object should still be accessible");
9037
9038        // Verify source ranges are still valid (not corrupted)
9039        // Just verify that the objects are still accessible and have the expected types
9040        match &arc_obj.kind {
9041            ObjectKind::Segment {
9042                segment: Segment::Arc(_),
9043            } => {}
9044            _ => panic!("Arc object should still be an arc segment"),
9045        }
9046        match &line_obj.kind {
9047            ObjectKind::Segment {
9048                segment: Segment::Line(_),
9049            } => {}
9050            _ => panic!("Line object should still be a line segment"),
9051        }
9052
9053        ctx.close().await;
9054        mock_ctx.close().await;
9055    }
9056
9057    #[tokio::test(flavor = "multi_thread")]
9058    async fn test_distance_two_points() {
9059        let initial_source = "\
9060sketch(on = XY) {
9061  point(at = [var 1, var 2])
9062  point(at = [var 3, var 4])
9063}
9064";
9065
9066        let program = Program::parse(initial_source).unwrap().0.unwrap();
9067
9068        let mut frontend = FrontendState::new();
9069
9070        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9071        let mock_ctx = ExecutorContext::new_mock(None).await;
9072        let version = Version(0);
9073
9074        frontend.hack_set_program(&ctx, program).await.unwrap();
9075        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9076        let sketch_id = sketch_object.id;
9077        let sketch = expect_sketch(sketch_object);
9078        let point0_id = *sketch.segments.first().unwrap();
9079        let point1_id = *sketch.segments.get(1).unwrap();
9080
9081        let constraint = Constraint::Distance(Distance {
9082            points: vec![point0_id.into(), point1_id.into()],
9083            distance: Number {
9084                value: 2.0,
9085                units: NumericSuffix::Mm,
9086            },
9087            label_position: None,
9088            source: Default::default(),
9089        });
9090        let (src_delta, scene_delta) = frontend
9091            .add_constraint(&mock_ctx, version, sketch_id, constraint)
9092            .await
9093            .unwrap();
9094        assert_eq!(
9095            src_delta.text.as_str(),
9096            // The lack indentation is a formatter bug.
9097            "\
9098sketch(on = XY) {
9099  point1 = point(at = [var 1, var 2])
9100  point2 = point(at = [var 3, var 4])
9101  distance([point1, point2]) == 2mm
9102}
9103"
9104        );
9105        assert_eq!(
9106            scene_delta.new_graph.objects.len(),
9107            5,
9108            "{:#?}",
9109            scene_delta.new_graph.objects
9110        );
9111
9112        ctx.close().await;
9113        mock_ctx.close().await;
9114    }
9115
9116    #[tokio::test(flavor = "multi_thread")]
9117    async fn test_distance_two_points_with_label() {
9118        let initial_source = "\
9119sketch(on = XY) {
9120  point(at = [var 1, var 2])
9121  point(at = [var 3, var 4])
9122}
9123";
9124
9125        let program = Program::parse(initial_source).unwrap().0.unwrap();
9126
9127        let mut frontend = FrontendState::new();
9128
9129        let mock_ctx = ExecutorContext::new_mock(None).await;
9130        let version = Version(0);
9131
9132        frontend.program = program.clone();
9133        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9134        frontend.update_state_after_exec(outcome, true);
9135        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9136        let sketch_id = sketch_object.id;
9137        let sketch = expect_sketch(sketch_object);
9138        let point0_id = *sketch.segments.first().unwrap();
9139        let point1_id = *sketch.segments.get(1).unwrap();
9140
9141        let label_position = Point2d {
9142            x: Number {
9143                value: 10.0,
9144                units: NumericSuffix::Mm,
9145            },
9146            y: Number {
9147                value: 11.0,
9148                units: NumericSuffix::Mm,
9149            },
9150        };
9151        let constraint = Constraint::Distance(Distance {
9152            points: vec![point0_id.into(), point1_id.into()],
9153            distance: Number {
9154                value: 2.0,
9155                units: NumericSuffix::Mm,
9156            },
9157            label_position: Some(label_position.clone()),
9158            source: Default::default(),
9159        });
9160        let (src_delta, scene_delta) = frontend
9161            .add_constraint(&mock_ctx, version, sketch_id, constraint)
9162            .await
9163            .unwrap();
9164        assert_eq!(
9165            src_delta.text.as_str(),
9166            "\
9167sketch(on = XY) {
9168  point1 = point(at = [var 1, var 2])
9169  point2 = point(at = [var 3, var 4])
9170  distance([point1, point2], labelPosition = [10mm, 11mm]) == 2mm
9171}
9172"
9173        );
9174
9175        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9176        let sketch = expect_sketch(sketch_object);
9177        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
9178        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9179            panic!("Expected constraint object");
9180        };
9181        let Constraint::Distance(distance) = constraint else {
9182            panic!("Expected distance constraint");
9183        };
9184        assert_eq!(distance.label_position, Some(label_position));
9185
9186        mock_ctx.close().await;
9187    }
9188
9189    #[tokio::test(flavor = "multi_thread")]
9190    async fn test_edit_distance_constraint_label_position() {
9191        let initial_source = "\
9192sketch(on = XY) {
9193  point(at = [var 1, var 2])
9194  point(at = [var 3, var 2])
9195}
9196";
9197
9198        let program = Program::parse(initial_source).unwrap().0.unwrap();
9199
9200        let mut frontend = FrontendState::new();
9201
9202        let mock_ctx = ExecutorContext::new_mock(None).await;
9203        let version = Version(0);
9204
9205        frontend.program = program.clone();
9206        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9207        frontend.update_state_after_exec(outcome, true);
9208        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9209        let sketch_id = sketch_object.id;
9210        let sketch = expect_sketch(sketch_object);
9211        let point0_id = *sketch.segments.first().unwrap();
9212        let point1_id = *sketch.segments.get(1).unwrap();
9213
9214        let constraint = Constraint::Distance(Distance {
9215            points: vec![point0_id.into(), point1_id.into()],
9216            distance: Number {
9217                value: 2.0,
9218                units: NumericSuffix::Mm,
9219            },
9220            label_position: None,
9221            source: Default::default(),
9222        });
9223        let (_, scene_delta) = frontend
9224            .add_constraint(&mock_ctx, version, sketch_id, constraint)
9225            .await
9226            .unwrap();
9227        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9228        let sketch = expect_sketch(sketch_object);
9229        let constraint_id = sketch.constraints[0];
9230        let label_position = Point2d {
9231            x: Number {
9232                value: 10.0,
9233                units: NumericSuffix::Mm,
9234            },
9235            y: Number {
9236                value: 11.0,
9237                units: NumericSuffix::Mm,
9238            },
9239        };
9240
9241        let (src_delta, scene_delta) = frontend
9242            .edit_distance_constraint_label_position(
9243                &mock_ctx,
9244                version,
9245                sketch_id,
9246                constraint_id,
9247                label_position.clone(),
9248                vec![],
9249            )
9250            .await
9251            .unwrap();
9252        assert_eq!(
9253            src_delta.text.as_str(),
9254            "\
9255sketch(on = XY) {
9256  point1 = point(at = [var 1mm, var 2mm])
9257  point2 = point(at = [var 3mm, var 2mm])
9258  distance([point1, point2], labelPosition = [10mm, 11mm]) == 2mm
9259}
9260"
9261        );
9262
9263        let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
9264        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9265            panic!("Expected constraint object");
9266        };
9267        let Constraint::Distance(distance) = constraint else {
9268            panic!("Expected distance constraint");
9269        };
9270        assert_eq!(distance.label_position, Some(label_position));
9271
9272        mock_ctx.close().await;
9273    }
9274
9275    #[tokio::test(flavor = "multi_thread")]
9276    async fn test_edit_distance_constraint_label_position_preserves_anchor_segment_solution() {
9277        let initial_source = "\
9278sketch(on = XY) {
9279  point1 = point(at = [var 0mm, var 0mm])
9280  point2 = point(at = [var 10mm, var 0mm])
9281  distance([point1, point2]) == 5mm
9282}
9283";
9284
9285        let program = Program::parse(initial_source).unwrap().0.unwrap();
9286        let mut frontend = FrontendState::new();
9287        let mock_ctx = ExecutorContext::new_mock(None).await;
9288        let version = Version(0);
9289
9290        frontend.program = program.clone();
9291        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9292        frontend.update_state_after_exec(outcome, true);
9293        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9294        let sketch_id = sketch_object.id;
9295        let sketch = expect_sketch(sketch_object);
9296        let point0_id = sketch.segments[0];
9297        let point1_id = sketch.segments[1];
9298        let constraint_id = sketch.constraints[0];
9299
9300        let edited_segments = vec![ExistingSegmentCtor {
9301            id: point0_id,
9302            ctor: SegmentCtor::Point(PointCtor {
9303                position: Point2d {
9304                    x: Expr::Var(Number {
9305                        value: 2.0,
9306                        units: NumericSuffix::Mm,
9307                    }),
9308                    y: Expr::Var(Number {
9309                        value: 1.0,
9310                        units: NumericSuffix::Mm,
9311                    }),
9312                },
9313            }),
9314        }];
9315        let (_, scene_delta) = frontend
9316            .edit_segments(&mock_ctx, version, sketch_id, edited_segments)
9317            .await
9318            .unwrap();
9319        let point0_after_segment_edit = point_position(&scene_delta.new_graph, point0_id);
9320        let point1_after_segment_edit = point_position(&scene_delta.new_graph, point1_id);
9321
9322        let label_position = Point2d {
9323            x: Number {
9324                value: 3.0,
9325                units: NumericSuffix::Mm,
9326            },
9327            y: Number {
9328                value: 4.0,
9329                units: NumericSuffix::Mm,
9330            },
9331        };
9332        let (_, scene_delta) = frontend
9333            .edit_distance_constraint_label_position(
9334                &mock_ctx,
9335                version,
9336                sketch_id,
9337                constraint_id,
9338                label_position,
9339                vec![point0_id],
9340            )
9341            .await
9342            .unwrap();
9343
9344        assert_point_position_close(
9345            point_position(&scene_delta.new_graph, point0_id),
9346            point0_after_segment_edit,
9347        );
9348        assert_point_position_close(
9349            point_position(&scene_delta.new_graph, point1_id),
9350            point1_after_segment_edit,
9351        );
9352
9353        mock_ctx.close().await;
9354    }
9355
9356    #[tokio::test(flavor = "multi_thread")]
9357    async fn test_horizontal_distance_two_points() {
9358        let initial_source = "\
9359sketch(on = XY) {
9360  point(at = [var 1, var 2])
9361  point(at = [var 3, var 4])
9362}
9363";
9364
9365        let program = Program::parse(initial_source).unwrap().0.unwrap();
9366
9367        let mut frontend = FrontendState::new();
9368
9369        let mock_ctx = ExecutorContext::new_mock(None).await;
9370        let version = Version(0);
9371
9372        frontend.program = program.clone();
9373        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9374        frontend.update_state_after_exec(outcome, true);
9375        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9376        let sketch_id = sketch_object.id;
9377        let sketch = expect_sketch(sketch_object);
9378        let point0_id = *sketch.segments.first().unwrap();
9379        let point1_id = *sketch.segments.get(1).unwrap();
9380        let label_position = Point2d {
9381            x: Number {
9382                value: 10.0,
9383                units: NumericSuffix::Mm,
9384            },
9385            y: Number {
9386                value: 11.0,
9387                units: NumericSuffix::Mm,
9388            },
9389        };
9390
9391        let constraint = Constraint::HorizontalDistance(Distance {
9392            points: vec![point0_id.into(), point1_id.into()],
9393            distance: Number {
9394                value: 2.0,
9395                units: NumericSuffix::Mm,
9396            },
9397            label_position: Some(label_position.clone()),
9398            source: Default::default(),
9399        });
9400        let (src_delta, scene_delta) = frontend
9401            .add_constraint(&mock_ctx, version, sketch_id, constraint)
9402            .await
9403            .unwrap();
9404        assert_eq!(
9405            src_delta.text.as_str(),
9406            // The lack indentation is a formatter bug.
9407            "\
9408sketch(on = XY) {
9409  point1 = point(at = [var 1, var 2])
9410  point2 = point(at = [var 3, var 4])
9411  horizontalDistance([point1, point2], labelPosition = [10mm, 11mm]) == 2mm
9412}
9413"
9414        );
9415        assert_eq!(
9416            scene_delta.new_graph.objects.len(),
9417            5,
9418            "{:#?}",
9419            scene_delta.new_graph.objects
9420        );
9421        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9422        let sketch = expect_sketch(sketch_object);
9423        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
9424        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9425            panic!("Expected constraint object");
9426        };
9427        let Constraint::HorizontalDistance(distance) = constraint else {
9428            panic!("Expected horizontal distance constraint");
9429        };
9430        assert_eq!(distance.label_position, Some(label_position));
9431
9432        mock_ctx.close().await;
9433    }
9434
9435    #[tokio::test(flavor = "multi_thread")]
9436    async fn test_radius_single_arc_segment() {
9437        let initial_source = "\
9438sketch(on = XY) {
9439  arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
9440}
9441";
9442
9443        let program = Program::parse(initial_source).unwrap().0.unwrap();
9444
9445        let mut frontend = FrontendState::new();
9446
9447        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9448        let mock_ctx = ExecutorContext::new_mock(None).await;
9449        let version = Version(0);
9450
9451        frontend.hack_set_program(&ctx, program).await.unwrap();
9452        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9453        let sketch_id = sketch_object.id;
9454        let sketch = expect_sketch(sketch_object);
9455        // Find the arc segment (not the points)
9456        let arc_id = sketch
9457            .segments
9458            .iter()
9459            .find(|&seg_id| {
9460                let obj = frontend.scene_graph.objects.get(seg_id.0);
9461                matches!(
9462                    obj.map(|o| &o.kind),
9463                    Some(ObjectKind::Segment {
9464                        segment: Segment::Arc(_)
9465                    })
9466                )
9467            })
9468            .unwrap();
9469
9470        let constraint = Constraint::Radius(Radius {
9471            arc: *arc_id,
9472            radius: Number {
9473                value: 5.0,
9474                units: NumericSuffix::Mm,
9475            },
9476            source: Default::default(),
9477        });
9478        let (src_delta, scene_delta) = frontend
9479            .add_constraint(&mock_ctx, version, sketch_id, constraint)
9480            .await
9481            .unwrap();
9482        assert_eq!(
9483            src_delta.text.as_str(),
9484            // The lack indentation is a formatter bug.
9485            "\
9486sketch(on = XY) {
9487  arc1 = arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
9488  radius(arc1) == 5mm
9489}
9490"
9491        );
9492        assert_eq!(
9493            scene_delta.new_graph.objects.len(),
9494            7, // Plane (0) + Sketch (1) + Start point (2) + End point (3) + Center point (4) + Arc (5) + Constraint (6)
9495            "{:#?}",
9496            scene_delta.new_graph.objects
9497        );
9498
9499        ctx.close().await;
9500        mock_ctx.close().await;
9501    }
9502
9503    #[tokio::test(flavor = "multi_thread")]
9504    async fn test_vertical_distance_two_points() {
9505        let initial_source = "\
9506sketch(on = XY) {
9507  point(at = [var 1, var 2])
9508  point(at = [var 3, var 4])
9509}
9510";
9511
9512        let program = Program::parse(initial_source).unwrap().0.unwrap();
9513
9514        let mut frontend = FrontendState::new();
9515
9516        let mock_ctx = ExecutorContext::new_mock(None).await;
9517        let version = Version(0);
9518
9519        frontend.program = program.clone();
9520        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9521        frontend.update_state_after_exec(outcome, true);
9522        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9523        let sketch_id = sketch_object.id;
9524        let sketch = expect_sketch(sketch_object);
9525        let point0_id = *sketch.segments.first().unwrap();
9526        let point1_id = *sketch.segments.get(1).unwrap();
9527        let label_position = Point2d {
9528            x: Number {
9529                value: 10.0,
9530                units: NumericSuffix::Mm,
9531            },
9532            y: Number {
9533                value: 11.0,
9534                units: NumericSuffix::Mm,
9535            },
9536        };
9537
9538        let constraint = Constraint::VerticalDistance(Distance {
9539            points: vec![point0_id.into(), point1_id.into()],
9540            distance: Number {
9541                value: 2.0,
9542                units: NumericSuffix::Mm,
9543            },
9544            label_position: Some(label_position.clone()),
9545            source: Default::default(),
9546        });
9547        let (src_delta, scene_delta) = frontend
9548            .add_constraint(&mock_ctx, version, sketch_id, constraint)
9549            .await
9550            .unwrap();
9551        assert_eq!(
9552            src_delta.text.as_str(),
9553            // The lack indentation is a formatter bug.
9554            "\
9555sketch(on = XY) {
9556  point1 = point(at = [var 1, var 2])
9557  point2 = point(at = [var 3, var 4])
9558  verticalDistance([point1, point2], labelPosition = [10mm, 11mm]) == 2mm
9559}
9560"
9561        );
9562        assert_eq!(
9563            scene_delta.new_graph.objects.len(),
9564            5,
9565            "{:#?}",
9566            scene_delta.new_graph.objects
9567        );
9568        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9569        let sketch = expect_sketch(sketch_object);
9570        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
9571        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9572            panic!("Expected constraint object");
9573        };
9574        let Constraint::VerticalDistance(distance) = constraint else {
9575            panic!("Expected vertical distance constraint");
9576        };
9577        assert_eq!(distance.label_position, Some(label_position));
9578
9579        mock_ctx.close().await;
9580    }
9581
9582    #[tokio::test(flavor = "multi_thread")]
9583    async fn test_add_fixed_standalone_point() {
9584        let initial_source = "\
9585sketch(on = XY) {
9586  point(at = [var 1, var 2])
9587}
9588";
9589
9590        let program = Program::parse(initial_source).unwrap().0.unwrap();
9591
9592        let mut frontend = FrontendState::new();
9593
9594        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9595        let mock_ctx = ExecutorContext::new_mock(None).await;
9596        let version = Version(0);
9597
9598        frontend.hack_set_program(&ctx, program).await.unwrap();
9599        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9600        let sketch_id = sketch_object.id;
9601        let sketch = expect_sketch(sketch_object);
9602        let point_id = *sketch.segments.first().unwrap();
9603
9604        let (src_delta, scene_delta) = frontend
9605            .add_constraint(
9606                &mock_ctx,
9607                version,
9608                sketch_id,
9609                Constraint::Fixed(Fixed {
9610                    points: vec![FixedPoint {
9611                        point: point_id,
9612                        position: Point2d {
9613                            x: Number {
9614                                value: 2.0,
9615                                units: NumericSuffix::Mm,
9616                            },
9617                            y: Number {
9618                                value: 3.0,
9619                                units: NumericSuffix::Mm,
9620                            },
9621                        },
9622                    }],
9623                }),
9624            )
9625            .await
9626            .unwrap();
9627        assert_eq!(
9628            src_delta.text.as_str(),
9629            "\
9630sketch(on = XY) {
9631  point1 = point(at = [var 1, var 2])
9632  fixed([point1, [2mm, 3mm]])
9633}
9634"
9635        );
9636        assert_eq!(
9637            scene_delta.new_graph.objects.len(),
9638            4,
9639            "{:#?}",
9640            scene_delta.new_graph.objects
9641        );
9642
9643        ctx.close().await;
9644        mock_ctx.close().await;
9645    }
9646
9647    #[tokio::test(flavor = "multi_thread")]
9648    async fn test_add_fixed_multiple_points() {
9649        let initial_source = "\
9650sketch(on = XY) {
9651  point(at = [var 1, var 2])
9652  point(at = [var 3, var 4])
9653}
9654";
9655
9656        let program = Program::parse(initial_source).unwrap().0.unwrap();
9657
9658        let mut frontend = FrontendState::new();
9659
9660        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9661        let mock_ctx = ExecutorContext::new_mock(None).await;
9662        let version = Version(0);
9663
9664        frontend.hack_set_program(&ctx, program).await.unwrap();
9665        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9666        let sketch_id = sketch_object.id;
9667        let sketch = expect_sketch(sketch_object);
9668        let point0_id = *sketch.segments.first().unwrap();
9669        let point1_id = *sketch.segments.get(1).unwrap();
9670
9671        let (src_delta, scene_delta) = frontend
9672            .add_constraint(
9673                &mock_ctx,
9674                version,
9675                sketch_id,
9676                Constraint::Fixed(Fixed {
9677                    points: vec![
9678                        FixedPoint {
9679                            point: point0_id,
9680                            position: Point2d {
9681                                x: Number {
9682                                    value: 2.0,
9683                                    units: NumericSuffix::Mm,
9684                                },
9685                                y: Number {
9686                                    value: 3.0,
9687                                    units: NumericSuffix::Mm,
9688                                },
9689                            },
9690                        },
9691                        FixedPoint {
9692                            point: point1_id,
9693                            position: Point2d {
9694                                x: Number {
9695                                    value: 4.0,
9696                                    units: NumericSuffix::Mm,
9697                                },
9698                                y: Number {
9699                                    value: 5.0,
9700                                    units: NumericSuffix::Mm,
9701                                },
9702                            },
9703                        },
9704                    ],
9705                }),
9706            )
9707            .await
9708            .unwrap();
9709        assert_eq!(
9710            src_delta.text.as_str(),
9711            "\
9712sketch(on = XY) {
9713  point1 = point(at = [var 1, var 2])
9714  point2 = point(at = [var 3, var 4])
9715  fixed([point1, [2mm, 3mm]])
9716  fixed([point2, [4mm, 5mm]])
9717}
9718"
9719        );
9720        assert_eq!(
9721            scene_delta.new_graph.objects.len(),
9722            6,
9723            "{:#?}",
9724            scene_delta.new_graph.objects
9725        );
9726
9727        ctx.close().await;
9728        mock_ctx.close().await;
9729    }
9730
9731    #[tokio::test(flavor = "multi_thread")]
9732    async fn test_add_fixed_owned_point() {
9733        let initial_source = "\
9734sketch(on = XY) {
9735  line(start = [var 1, var 2], end = [var 3, var 4])
9736}
9737";
9738
9739        let program = Program::parse(initial_source).unwrap().0.unwrap();
9740
9741        let mut frontend = FrontendState::new();
9742
9743        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9744        let mock_ctx = ExecutorContext::new_mock(None).await;
9745        let version = Version(0);
9746
9747        frontend.hack_set_program(&ctx, program).await.unwrap();
9748        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9749        let sketch_id = sketch_object.id;
9750        let sketch = expect_sketch(sketch_object);
9751        let line_start_id = *sketch.segments.first().unwrap();
9752
9753        let (src_delta, scene_delta) = frontend
9754            .add_constraint(
9755                &mock_ctx,
9756                version,
9757                sketch_id,
9758                Constraint::Fixed(Fixed {
9759                    points: vec![FixedPoint {
9760                        point: line_start_id,
9761                        position: Point2d {
9762                            x: Number {
9763                                value: 2.0,
9764                                units: NumericSuffix::Mm,
9765                            },
9766                            y: Number {
9767                                value: 3.0,
9768                                units: NumericSuffix::Mm,
9769                            },
9770                        },
9771                    }],
9772                }),
9773            )
9774            .await
9775            .unwrap();
9776        assert_eq!(
9777            src_delta.text.as_str(),
9778            "\
9779sketch(on = XY) {
9780  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9781  fixed([line1.start, [2mm, 3mm]])
9782}
9783"
9784        );
9785        assert_eq!(
9786            scene_delta.new_graph.objects.len(),
9787            6,
9788            "{:#?}",
9789            scene_delta.new_graph.objects
9790        );
9791
9792        ctx.close().await;
9793        mock_ctx.close().await;
9794    }
9795
9796    #[tokio::test(flavor = "multi_thread")]
9797    async fn test_radius_error_cases() {
9798        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9799        let mock_ctx = ExecutorContext::new_mock(None).await;
9800        let version = Version(0);
9801
9802        // Test: Single point should error
9803        let initial_source_point = "\
9804sketch(on = XY) {
9805  point(at = [var 1, var 2])
9806}
9807";
9808        let program_point = Program::parse(initial_source_point).unwrap().0.unwrap();
9809        let mut frontend_point = FrontendState::new();
9810        frontend_point.hack_set_program(&ctx, program_point).await.unwrap();
9811        let sketch_object_point = find_first_sketch_object(&frontend_point.scene_graph).unwrap();
9812        let sketch_id_point = sketch_object_point.id;
9813        let sketch_point = expect_sketch(sketch_object_point);
9814        let point_id = *sketch_point.segments.first().unwrap();
9815
9816        let constraint_point = Constraint::Radius(Radius {
9817            arc: point_id,
9818            radius: Number {
9819                value: 5.0,
9820                units: NumericSuffix::Mm,
9821            },
9822            source: Default::default(),
9823        });
9824        let result_point = frontend_point
9825            .add_constraint(&mock_ctx, version, sketch_id_point, constraint_point)
9826            .await;
9827        assert!(result_point.is_err(), "Single point should error for radius");
9828
9829        // Test: Single line segment should error (only arc segments supported)
9830        let initial_source_line = "\
9831sketch(on = XY) {
9832  line(start = [var 1, var 2], end = [var 3, var 4])
9833}
9834";
9835        let program_line = Program::parse(initial_source_line).unwrap().0.unwrap();
9836        let mut frontend_line = FrontendState::new();
9837        frontend_line.hack_set_program(&ctx, program_line).await.unwrap();
9838        let sketch_object_line = find_first_sketch_object(&frontend_line.scene_graph).unwrap();
9839        let sketch_id_line = sketch_object_line.id;
9840        let sketch_line = expect_sketch(sketch_object_line);
9841        let line_id = *sketch_line.segments.first().unwrap();
9842
9843        let constraint_line = Constraint::Radius(Radius {
9844            arc: line_id,
9845            radius: Number {
9846                value: 5.0,
9847                units: NumericSuffix::Mm,
9848            },
9849            source: Default::default(),
9850        });
9851        let result_line = frontend_line
9852            .add_constraint(&mock_ctx, version, sketch_id_line, constraint_line)
9853            .await;
9854        assert!(result_line.is_err(), "Single line segment should error for radius");
9855
9856        ctx.close().await;
9857        mock_ctx.close().await;
9858    }
9859
9860    #[tokio::test(flavor = "multi_thread")]
9861    async fn test_diameter_single_arc_segment() {
9862        let initial_source = "\
9863sketch(on = XY) {
9864  arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
9865}
9866";
9867
9868        let program = Program::parse(initial_source).unwrap().0.unwrap();
9869
9870        let mut frontend = FrontendState::new();
9871
9872        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9873        let mock_ctx = ExecutorContext::new_mock(None).await;
9874        let version = Version(0);
9875
9876        frontend.hack_set_program(&ctx, program).await.unwrap();
9877        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9878        let sketch_id = sketch_object.id;
9879        let sketch = expect_sketch(sketch_object);
9880        // Find the arc segment (not the points)
9881        let arc_id = sketch
9882            .segments
9883            .iter()
9884            .find(|&seg_id| {
9885                let obj = frontend.scene_graph.objects.get(seg_id.0);
9886                matches!(
9887                    obj.map(|o| &o.kind),
9888                    Some(ObjectKind::Segment {
9889                        segment: Segment::Arc(_)
9890                    })
9891                )
9892            })
9893            .unwrap();
9894
9895        let constraint = Constraint::Diameter(Diameter {
9896            arc: *arc_id,
9897            diameter: Number {
9898                value: 10.0,
9899                units: NumericSuffix::Mm,
9900            },
9901            source: Default::default(),
9902        });
9903        let (src_delta, scene_delta) = frontend
9904            .add_constraint(&mock_ctx, version, sketch_id, constraint)
9905            .await
9906            .unwrap();
9907        assert_eq!(
9908            src_delta.text.as_str(),
9909            // The lack indentation is a formatter bug.
9910            "\
9911sketch(on = XY) {
9912  arc1 = arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
9913  diameter(arc1) == 10mm
9914}
9915"
9916        );
9917        assert_eq!(
9918            scene_delta.new_graph.objects.len(),
9919            7, // Plane (0) + Sketch (1) + Start point (2) + End point (3) + Center point (4) + Arc (5) + Constraint (6)
9920            "{:#?}",
9921            scene_delta.new_graph.objects
9922        );
9923
9924        ctx.close().await;
9925        mock_ctx.close().await;
9926    }
9927
9928    #[tokio::test(flavor = "multi_thread")]
9929    async fn test_diameter_error_cases() {
9930        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9931        let mock_ctx = ExecutorContext::new_mock(None).await;
9932        let version = Version(0);
9933
9934        // Test: Single point should error
9935        let initial_source_point = "\
9936sketch(on = XY) {
9937  point(at = [var 1, var 2])
9938}
9939";
9940        let program_point = Program::parse(initial_source_point).unwrap().0.unwrap();
9941        let mut frontend_point = FrontendState::new();
9942        frontend_point.hack_set_program(&ctx, program_point).await.unwrap();
9943        let sketch_object_point = find_first_sketch_object(&frontend_point.scene_graph).unwrap();
9944        let sketch_id_point = sketch_object_point.id;
9945        let sketch_point = expect_sketch(sketch_object_point);
9946        let point_id = *sketch_point.segments.first().unwrap();
9947
9948        let constraint_point = Constraint::Diameter(Diameter {
9949            arc: point_id,
9950            diameter: Number {
9951                value: 10.0,
9952                units: NumericSuffix::Mm,
9953            },
9954            source: Default::default(),
9955        });
9956        let result_point = frontend_point
9957            .add_constraint(&mock_ctx, version, sketch_id_point, constraint_point)
9958            .await;
9959        assert!(result_point.is_err(), "Single point should error for diameter");
9960
9961        // Test: Single line segment should error (only arc segments supported)
9962        let initial_source_line = "\
9963sketch(on = XY) {
9964  line(start = [var 1, var 2], end = [var 3, var 4])
9965}
9966";
9967        let program_line = Program::parse(initial_source_line).unwrap().0.unwrap();
9968        let mut frontend_line = FrontendState::new();
9969        frontend_line.hack_set_program(&ctx, program_line).await.unwrap();
9970        let sketch_object_line = find_first_sketch_object(&frontend_line.scene_graph).unwrap();
9971        let sketch_id_line = sketch_object_line.id;
9972        let sketch_line = expect_sketch(sketch_object_line);
9973        let line_id = *sketch_line.segments.first().unwrap();
9974
9975        let constraint_line = Constraint::Diameter(Diameter {
9976            arc: line_id,
9977            diameter: Number {
9978                value: 10.0,
9979                units: NumericSuffix::Mm,
9980            },
9981            source: Default::default(),
9982        });
9983        let result_line = frontend_line
9984            .add_constraint(&mock_ctx, version, sketch_id_line, constraint_line)
9985            .await;
9986        assert!(result_line.is_err(), "Single line segment should error for diameter");
9987
9988        ctx.close().await;
9989        mock_ctx.close().await;
9990    }
9991
9992    #[tokio::test(flavor = "multi_thread")]
9993    async fn test_line_horizontal() {
9994        let initial_source = "\
9995sketch(on = XY) {
9996  line(start = [var 1, var 2], end = [var 3, var 4])
9997}
9998";
9999
10000        let program = Program::parse(initial_source).unwrap().0.unwrap();
10001
10002        let mut frontend = FrontendState::new();
10003
10004        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10005        let mock_ctx = ExecutorContext::new_mock(None).await;
10006        let version = Version(0);
10007
10008        frontend.hack_set_program(&ctx, program).await.unwrap();
10009        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10010        let sketch_id = sketch_object.id;
10011        let sketch = expect_sketch(sketch_object);
10012        let line1_id = *sketch.segments.get(2).unwrap();
10013
10014        let constraint = Constraint::Horizontal(Horizontal::Line { line: line1_id });
10015        let (src_delta, scene_delta) = frontend
10016            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10017            .await
10018            .unwrap();
10019        assert_eq!(
10020            src_delta.text.as_str(),
10021            "\
10022sketch(on = XY) {
10023  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10024  horizontal(line1)
10025}
10026"
10027        );
10028        assert_eq!(
10029            scene_delta.new_graph.objects.len(),
10030            6,
10031            "{:#?}",
10032            scene_delta.new_graph.objects
10033        );
10034
10035        ctx.close().await;
10036        mock_ctx.close().await;
10037    }
10038
10039    #[tokio::test(flavor = "multi_thread")]
10040    async fn test_line_vertical() {
10041        let initial_source = "\
10042sketch(on = XY) {
10043  line(start = [var 1, var 2], end = [var 3, var 4])
10044}
10045";
10046
10047        let program = Program::parse(initial_source).unwrap().0.unwrap();
10048
10049        let mut frontend = FrontendState::new();
10050
10051        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10052        let mock_ctx = ExecutorContext::new_mock(None).await;
10053        let version = Version(0);
10054
10055        frontend.hack_set_program(&ctx, program).await.unwrap();
10056        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10057        let sketch_id = sketch_object.id;
10058        let sketch = expect_sketch(sketch_object);
10059        let line1_id = *sketch.segments.get(2).unwrap();
10060
10061        let constraint = Constraint::Vertical(Vertical::Line { line: line1_id });
10062        let (src_delta, scene_delta) = frontend
10063            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10064            .await
10065            .unwrap();
10066        assert_eq!(
10067            src_delta.text.as_str(),
10068            "\
10069sketch(on = XY) {
10070  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10071  vertical(line1)
10072}
10073"
10074        );
10075        assert_eq!(
10076            scene_delta.new_graph.objects.len(),
10077            6,
10078            "{:#?}",
10079            scene_delta.new_graph.objects
10080        );
10081
10082        ctx.close().await;
10083        mock_ctx.close().await;
10084    }
10085
10086    #[tokio::test(flavor = "multi_thread")]
10087    async fn test_points_vertical() {
10088        let initial_source = "\
10089sketch001 = sketch(on = XY) {
10090  p0 = point(at = [var -2.23mm, var 3.1mm])
10091  pf = point(at = [4, 4])
10092}
10093";
10094
10095        let program = Program::parse(initial_source).unwrap().0.unwrap();
10096
10097        let mut frontend = FrontendState::new();
10098
10099        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10100        let mock_ctx = ExecutorContext::new_mock(None).await;
10101        let version = Version(0);
10102
10103        frontend.hack_set_program(&ctx, program).await.unwrap();
10104        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10105        let sketch_id = sketch_object.id;
10106        let sketch = expect_sketch(sketch_object);
10107        let point_ids = vec![
10108            sketch.segments.first().unwrap().to_owned(),
10109            sketch.segments.get(1).unwrap().to_owned(),
10110        ];
10111
10112        let constraint = Constraint::Vertical(Vertical::Points {
10113            points: point_ids.into_iter().map(ConstraintSegment::from).collect(),
10114        });
10115        let (src_delta, scene_delta) = frontend
10116            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10117            .await
10118            .unwrap();
10119        assert_eq!(
10120            src_delta.text.as_str(),
10121            "\
10122sketch001 = sketch(on = XY) {
10123  p0 = point(at = [var -2.23mm, var 3.1mm])
10124  pf = point(at = [4, 4])
10125  vertical([p0, pf])
10126}
10127"
10128        );
10129        assert_eq!(
10130            scene_delta.new_graph.objects.len(),
10131            5,
10132            "{:#?}",
10133            scene_delta.new_graph.objects
10134        );
10135
10136        ctx.close().await;
10137        mock_ctx.close().await;
10138    }
10139
10140    #[tokio::test(flavor = "multi_thread")]
10141    async fn test_points_horizontal() {
10142        let initial_source = "\
10143sketch001 = sketch(on = XY) {
10144  p0 = point(at = [var -2.23mm, var 3.1mm])
10145  pf = point(at = [4, 4])
10146}
10147";
10148
10149        let program = Program::parse(initial_source).unwrap().0.unwrap();
10150
10151        let mut frontend = FrontendState::new();
10152
10153        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10154        let mock_ctx = ExecutorContext::new_mock(None).await;
10155        let version = Version(0);
10156
10157        frontend.hack_set_program(&ctx, program).await.unwrap();
10158        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10159        let sketch_id = sketch_object.id;
10160        let sketch = expect_sketch(sketch_object);
10161        let point_ids = vec![
10162            sketch.segments.first().unwrap().to_owned(),
10163            sketch.segments.get(1).unwrap().to_owned(),
10164        ];
10165
10166        let constraint = Constraint::Horizontal(Horizontal::Points {
10167            points: point_ids.into_iter().map(ConstraintSegment::from).collect(),
10168        });
10169        let (src_delta, scene_delta) = frontend
10170            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10171            .await
10172            .unwrap();
10173        assert_eq!(
10174            src_delta.text.as_str(),
10175            "\
10176sketch001 = sketch(on = XY) {
10177  p0 = point(at = [var -2.23mm, var 3.1mm])
10178  pf = point(at = [4, 4])
10179  horizontal([p0, pf])
10180}
10181"
10182        );
10183        assert_eq!(
10184            scene_delta.new_graph.objects.len(),
10185            5,
10186            "{:#?}",
10187            scene_delta.new_graph.objects
10188        );
10189
10190        ctx.close().await;
10191        mock_ctx.close().await;
10192    }
10193
10194    #[tokio::test(flavor = "multi_thread")]
10195    async fn test_point_horizontal_with_origin() {
10196        let initial_source = "\
10197sketch001 = sketch(on = XY) {
10198  p0 = point(at = [var -2.23mm, var 3.1mm])
10199}
10200";
10201
10202        let program = Program::parse(initial_source).unwrap().0.unwrap();
10203
10204        let mut frontend = FrontendState::new();
10205
10206        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10207        let mock_ctx = ExecutorContext::new_mock(None).await;
10208        let version = Version(0);
10209
10210        frontend.hack_set_program(&ctx, program).await.unwrap();
10211        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10212        let sketch_id = sketch_object.id;
10213        let sketch = expect_sketch(sketch_object);
10214        let point_id = *sketch.segments.first().unwrap();
10215
10216        let constraint = Constraint::Horizontal(Horizontal::Points {
10217            points: vec![ConstraintSegment::from(point_id), ConstraintSegment::ORIGIN],
10218        });
10219        let (src_delta, scene_delta) = frontend
10220            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10221            .await
10222            .unwrap();
10223        assert_eq!(
10224            src_delta.text.as_str(),
10225            "\
10226sketch001 = sketch(on = XY) {
10227  p0 = point(at = [var -2.23mm, var 3.1mm])
10228  horizontal([p0, ORIGIN])
10229}
10230"
10231        );
10232        assert_eq!(
10233            scene_delta.new_graph.objects.len(),
10234            4,
10235            "{:#?}",
10236            scene_delta.new_graph.objects
10237        );
10238
10239        ctx.close().await;
10240        mock_ctx.close().await;
10241    }
10242
10243    #[tokio::test(flavor = "multi_thread")]
10244    async fn test_lines_equal_length() {
10245        let initial_source = "\
10246sketch(on = XY) {
10247  line(start = [var 1, var 2], end = [var 3, var 4])
10248  line(start = [var 5, var 6], end = [var 7, var 8])
10249}
10250";
10251
10252        let program = Program::parse(initial_source).unwrap().0.unwrap();
10253
10254        let mut frontend = FrontendState::new();
10255
10256        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10257        let mock_ctx = ExecutorContext::new_mock(None).await;
10258        let version = Version(0);
10259
10260        frontend.hack_set_program(&ctx, program).await.unwrap();
10261        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10262        let sketch_id = sketch_object.id;
10263        let sketch = expect_sketch(sketch_object);
10264        let line1_id = *sketch.segments.get(2).unwrap();
10265        let line2_id = *sketch.segments.get(5).unwrap();
10266
10267        let constraint = Constraint::LinesEqualLength(LinesEqualLength {
10268            lines: vec![line1_id, line2_id],
10269        });
10270        let (src_delta, scene_delta) = frontend
10271            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10272            .await
10273            .unwrap();
10274        assert_eq!(
10275            src_delta.text.as_str(),
10276            "\
10277sketch(on = XY) {
10278  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10279  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10280  equalLength([line1, line2])
10281}
10282"
10283        );
10284        assert_eq!(
10285            scene_delta.new_graph.objects.len(),
10286            9,
10287            "{:#?}",
10288            scene_delta.new_graph.objects
10289        );
10290
10291        ctx.close().await;
10292        mock_ctx.close().await;
10293    }
10294
10295    #[tokio::test(flavor = "multi_thread")]
10296    async fn test_add_constraint_multi_line_equal_length() {
10297        let initial_source = "\
10298sketch(on = XY) {
10299  line(start = [var 1, var 2], end = [var 3, var 4])
10300  line(start = [var 5, var 6], end = [var 7, var 8])
10301  line(start = [var 9, var 10], end = [var 11, var 12])
10302}
10303";
10304
10305        let program = Program::parse(initial_source).unwrap().0.unwrap();
10306
10307        let mut frontend = FrontendState::new();
10308        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10309        let mock_ctx = ExecutorContext::new_mock(None).await;
10310        let version = Version(0);
10311
10312        frontend.hack_set_program(&ctx, program).await.unwrap();
10313        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10314        let sketch_id = sketch_object.id;
10315        let sketch = expect_sketch(sketch_object);
10316        let line1_id = *sketch.segments.get(2).unwrap();
10317        let line2_id = *sketch.segments.get(5).unwrap();
10318        let line3_id = *sketch.segments.get(8).unwrap();
10319
10320        let constraint = Constraint::LinesEqualLength(LinesEqualLength {
10321            lines: vec![line1_id, line2_id, line3_id],
10322        });
10323        let (src_delta, scene_delta) = frontend
10324            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10325            .await
10326            .unwrap();
10327        assert_eq!(
10328            src_delta.text.as_str(),
10329            "\
10330sketch(on = XY) {
10331  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10332  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10333  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10334  equalLength([line1, line2, line3])
10335}
10336"
10337        );
10338        let constraints = scene_delta
10339            .new_graph
10340            .objects
10341            .iter()
10342            .filter_map(|obj| {
10343                let ObjectKind::Constraint { constraint } = &obj.kind else {
10344                    return None;
10345                };
10346                Some(constraint)
10347            })
10348            .collect::<Vec<_>>();
10349
10350        assert_eq!(constraints.len(), 1, "{:#?}", frontend.scene_graph.objects);
10351        let Constraint::LinesEqualLength(lines_equal_length) = constraints[0] else {
10352            panic!("expected equal length constraint, got {:?}", constraints[0]);
10353        };
10354        assert_eq!(lines_equal_length.lines.len(), 3);
10355
10356        ctx.close().await;
10357        mock_ctx.close().await;
10358    }
10359
10360    #[tokio::test(flavor = "multi_thread")]
10361    async fn test_lines_parallel() {
10362        let initial_source = "\
10363sketch(on = XY) {
10364  line(start = [var 1, var 2], end = [var 3, var 4])
10365  line(start = [var 5, var 6], end = [var 7, var 8])
10366}
10367";
10368
10369        let program = Program::parse(initial_source).unwrap().0.unwrap();
10370
10371        let mut frontend = FrontendState::new();
10372
10373        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10374        let mock_ctx = ExecutorContext::new_mock(None).await;
10375        let version = Version(0);
10376
10377        frontend.hack_set_program(&ctx, program).await.unwrap();
10378        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10379        let sketch_id = sketch_object.id;
10380        let sketch = expect_sketch(sketch_object);
10381        let line1_id = *sketch.segments.get(2).unwrap();
10382        let line2_id = *sketch.segments.get(5).unwrap();
10383
10384        let constraint = Constraint::Parallel(Parallel {
10385            lines: vec![line1_id, line2_id],
10386        });
10387        let (src_delta, scene_delta) = frontend
10388            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10389            .await
10390            .unwrap();
10391        assert_eq!(
10392            src_delta.text.as_str(),
10393            "\
10394sketch(on = XY) {
10395  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10396  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10397  parallel([line1, line2])
10398}
10399"
10400        );
10401        assert_eq!(
10402            scene_delta.new_graph.objects.len(),
10403            9,
10404            "{:#?}",
10405            scene_delta.new_graph.objects
10406        );
10407
10408        ctx.close().await;
10409        mock_ctx.close().await;
10410    }
10411
10412    #[tokio::test(flavor = "multi_thread")]
10413    async fn test_lines_parallel_multiline() {
10414        let initial_source = "\
10415sketch(on = XY) {
10416  line(start = [var 1, var 2], end = [var 3, var 4])
10417  line(start = [var 5, var 6], end = [var 7, var 8])
10418  line(start = [var 9, var 10], end = [var 11, var 12])
10419}
10420";
10421
10422        let program = Program::parse(initial_source).unwrap().0.unwrap();
10423
10424        let mut frontend = FrontendState::new();
10425
10426        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10427        let mock_ctx = ExecutorContext::new_mock(None).await;
10428        let version = Version(0);
10429
10430        frontend.hack_set_program(&ctx, program).await.unwrap();
10431        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10432        let sketch_id = sketch_object.id;
10433        let sketch = expect_sketch(sketch_object);
10434        let line1_id = *sketch.segments.get(2).unwrap();
10435        let line2_id = *sketch.segments.get(5).unwrap();
10436        let line3_id = *sketch.segments.get(8).unwrap();
10437
10438        let constraint = Constraint::Parallel(Parallel {
10439            lines: vec![line1_id, line2_id, line3_id],
10440        });
10441        let (src_delta, scene_delta) = frontend
10442            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10443            .await
10444            .unwrap();
10445        assert_eq!(
10446            src_delta.text.as_str(),
10447            "\
10448sketch(on = XY) {
10449  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10450  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10451  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10452  parallel([line1, line2, line3])
10453}
10454"
10455        );
10456
10457        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10458        let sketch = expect_sketch(sketch_object);
10459        assert_eq!(sketch.constraints.len(), 1);
10460
10461        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10462        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10463            panic!("Expected constraint object");
10464        };
10465        let Constraint::Parallel(parallel) = constraint else {
10466            panic!("Expected parallel constraint");
10467        };
10468        assert_eq!(parallel.lines.len(), 3);
10469
10470        ctx.close().await;
10471        mock_ctx.close().await;
10472    }
10473
10474    #[tokio::test(flavor = "multi_thread")]
10475    async fn test_lines_perpendicular() {
10476        let initial_source = "\
10477sketch(on = XY) {
10478  line(start = [var 1, var 2], end = [var 3, var 4])
10479  line(start = [var 5, var 6], end = [var 7, var 8])
10480}
10481";
10482
10483        let program = Program::parse(initial_source).unwrap().0.unwrap();
10484
10485        let mut frontend = FrontendState::new();
10486
10487        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10488        let mock_ctx = ExecutorContext::new_mock(None).await;
10489        let version = Version(0);
10490
10491        frontend.hack_set_program(&ctx, program).await.unwrap();
10492        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10493        let sketch_id = sketch_object.id;
10494        let sketch = expect_sketch(sketch_object);
10495        let line1_id = *sketch.segments.get(2).unwrap();
10496        let line2_id = *sketch.segments.get(5).unwrap();
10497
10498        let constraint = Constraint::Perpendicular(Perpendicular {
10499            lines: vec![line1_id, line2_id],
10500        });
10501        let (src_delta, scene_delta) = frontend
10502            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10503            .await
10504            .unwrap();
10505        assert_eq!(
10506            src_delta.text.as_str(),
10507            "\
10508sketch(on = XY) {
10509  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10510  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10511  perpendicular([line1, line2])
10512}
10513"
10514        );
10515        assert_eq!(
10516            scene_delta.new_graph.objects.len(),
10517            9,
10518            "{:#?}",
10519            scene_delta.new_graph.objects
10520        );
10521
10522        ctx.close().await;
10523        mock_ctx.close().await;
10524    }
10525
10526    #[tokio::test(flavor = "multi_thread")]
10527    async fn test_lines_angle() {
10528        let initial_source = "\
10529sketch(on = XY) {
10530  line(start = [var 1, var 2], end = [var 3, var 4])
10531  line(start = [var 5, var 6], end = [var 7, var 8])
10532}
10533";
10534
10535        let program = Program::parse(initial_source).unwrap().0.unwrap();
10536
10537        let mut frontend = FrontendState::new();
10538
10539        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10540        let mock_ctx = ExecutorContext::new_mock(None).await;
10541        let version = Version(0);
10542
10543        frontend.hack_set_program(&ctx, program).await.unwrap();
10544        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10545        let sketch_id = sketch_object.id;
10546        let sketch = expect_sketch(sketch_object);
10547        let line1_id = *sketch.segments.get(2).unwrap();
10548        let line2_id = *sketch.segments.get(5).unwrap();
10549
10550        let constraint = Constraint::Angle(Angle {
10551            lines: vec![line1_id, line2_id],
10552            angle: Number {
10553                value: 30.0,
10554                units: NumericSuffix::Deg,
10555            },
10556            source: Default::default(),
10557        });
10558        let (src_delta, scene_delta) = frontend
10559            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10560            .await
10561            .unwrap();
10562        assert_eq!(
10563            src_delta.text.as_str(),
10564            // The lack indentation is a formatter bug.
10565            "\
10566sketch(on = XY) {
10567  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10568  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10569  angle([line1, line2]) == 30deg
10570}
10571"
10572        );
10573        assert_eq!(
10574            scene_delta.new_graph.objects.len(),
10575            9,
10576            "{:#?}",
10577            scene_delta.new_graph.objects
10578        );
10579
10580        ctx.close().await;
10581        mock_ctx.close().await;
10582    }
10583
10584    #[tokio::test(flavor = "multi_thread")]
10585    async fn test_segments_tangent() {
10586        let initial_source = "\
10587sketch(on = XY) {
10588  line(start = [var 1, var 2], end = [var 3, var 4])
10589  arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
10590}
10591";
10592
10593        let program = Program::parse(initial_source).unwrap().0.unwrap();
10594
10595        let mut frontend = FrontendState::new();
10596
10597        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10598        let mock_ctx = ExecutorContext::new_mock(None).await;
10599        let version = Version(0);
10600
10601        frontend.hack_set_program(&ctx, program).await.unwrap();
10602        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10603        let sketch_id = sketch_object.id;
10604        let sketch = expect_sketch(sketch_object);
10605        let line1_id = *sketch.segments.get(2).unwrap();
10606        let arc1_id = *sketch.segments.get(6).unwrap();
10607
10608        let constraint = Constraint::Tangent(Tangent {
10609            input: vec![line1_id, arc1_id],
10610        });
10611        let (src_delta, scene_delta) = frontend
10612            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10613            .await
10614            .unwrap();
10615        assert_eq!(
10616            src_delta.text.as_str(),
10617            "\
10618sketch(on = XY) {
10619  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10620  arc1 = arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
10621  tangent([line1, arc1])
10622}
10623"
10624        );
10625        assert_eq!(
10626            scene_delta.new_graph.objects.len(),
10627            10,
10628            "{:#?}",
10629            scene_delta.new_graph.objects
10630        );
10631
10632        ctx.close().await;
10633        mock_ctx.close().await;
10634    }
10635
10636    #[tokio::test(flavor = "multi_thread")]
10637    async fn test_point_midpoint() {
10638        let initial_source = "\
10639sketch(on = XY) {
10640  point(at = [var 1, var 1])
10641  line(start = [var 0, var 0], end = [var 6, var 4])
10642}
10643";
10644
10645        let program = Program::parse(initial_source).unwrap().0.unwrap();
10646
10647        let mut frontend = FrontendState::new();
10648
10649        let ctx = ExecutorContext::new_mock(None).await;
10650        let version = Version(0);
10651
10652        frontend.program = program.clone();
10653        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10654        frontend.update_state_after_exec(outcome, true);
10655        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10656        let sketch_id = sketch_object.id;
10657        let sketch = expect_sketch(sketch_object);
10658        let point_id = *sketch.segments.first().unwrap();
10659        let line_id = *sketch.segments.get(3).unwrap();
10660
10661        let constraint = Constraint::Midpoint(Midpoint {
10662            point: point_id,
10663            segment: line_id,
10664        });
10665        let (src_delta, scene_delta) = frontend
10666            .add_constraint(&ctx, version, sketch_id, constraint)
10667            .await
10668            .unwrap();
10669        assert_eq!(
10670            src_delta.text.as_str(),
10671            "\
10672sketch(on = XY) {
10673  point1 = point(at = [var 1, var 1])
10674  line1 = line(start = [var 0, var 0], end = [var 6, var 4])
10675  midpoint(line1, point = point1)
10676}
10677"
10678        );
10679        assert_eq!(
10680            scene_delta.new_graph.objects.len(),
10681            7,
10682            "{:#?}",
10683            scene_delta.new_graph.objects
10684        );
10685
10686        ctx.close().await;
10687    }
10688
10689    #[tokio::test(flavor = "multi_thread")]
10690    async fn test_segments_symmetric() {
10691        let initial_source = "\
10692sketch(on = XY) {
10693  line(start = [var 0, var 0], end = [var 0, var 4])
10694  line(start = [var 4, var 0], end = [var 4, var 4])
10695  line(start = [var 2, var -1], end = [var 2, var 5])
10696}
10697";
10698
10699        let program = Program::parse(initial_source).unwrap().0.unwrap();
10700
10701        let mut frontend = FrontendState::new();
10702
10703        let ctx = ExecutorContext::new_mock(None).await;
10704        let version = Version(0);
10705
10706        frontend.program = program.clone();
10707        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10708        frontend.update_state_after_exec(outcome, true);
10709        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10710        let sketch_id = sketch_object.id;
10711        let sketch = expect_sketch(sketch_object);
10712        let line1_id = *sketch.segments.get(2).unwrap();
10713        let line2_id = *sketch.segments.get(5).unwrap();
10714        let axis_id = *sketch.segments.get(8).unwrap();
10715
10716        let constraint = Constraint::Symmetric(Symmetric {
10717            input: vec![line1_id, line2_id],
10718            axis: axis_id,
10719        });
10720        let (src_delta, scene_delta) = frontend
10721            .add_constraint(&ctx, version, sketch_id, constraint)
10722            .await
10723            .unwrap();
10724        assert_eq!(
10725            src_delta.text.as_str(),
10726            "\
10727sketch(on = XY) {
10728  line1 = line(start = [var 0, var 0], end = [var 0, var 4])
10729  line2 = line(start = [var 4, var 0], end = [var 4, var 4])
10730  line3 = line(start = [var 2, var -1], end = [var 2, var 5])
10731  symmetric([line1, line2], axis = line3)
10732}
10733"
10734        );
10735        assert_eq!(
10736            scene_delta.new_graph.objects.len(),
10737            12,
10738            "{:#?}",
10739            scene_delta.new_graph.objects
10740        );
10741
10742        ctx.close().await;
10743    }
10744
10745    #[tokio::test(flavor = "multi_thread")]
10746    async fn test_point_arc_midpoint() {
10747        let initial_source = "\
10748sketch(on = XY) {
10749  point(at = [var 6, var 3])
10750  arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
10751}
10752";
10753
10754        let program = Program::parse(initial_source).unwrap().0.unwrap();
10755
10756        let mut frontend = FrontendState::new();
10757
10758        let ctx = ExecutorContext::new_mock(None).await;
10759        let version = Version(0);
10760
10761        frontend.program = program.clone();
10762        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10763        frontend.update_state_after_exec(outcome, true);
10764        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10765        let sketch_id = sketch_object.id;
10766        let sketch = expect_sketch(sketch_object);
10767        let point_id = *sketch.segments.first().unwrap();
10768        let arc_id = *sketch.segments.get(4).unwrap();
10769
10770        let constraint = Constraint::Midpoint(Midpoint {
10771            point: point_id,
10772            segment: arc_id,
10773        });
10774        let (src_delta, scene_delta) = frontend
10775            .add_constraint(&ctx, version, sketch_id, constraint)
10776            .await
10777            .unwrap();
10778        assert_eq!(
10779            src_delta.text.as_str(),
10780            "\
10781sketch(on = XY) {
10782  point1 = point(at = [var 6, var 3])
10783  arc1 = arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
10784  midpoint(arc1, point = point1)
10785}
10786"
10787        );
10788        assert_eq!(
10789            scene_delta.new_graph.objects.len(),
10790            8,
10791            "{:#?}",
10792            scene_delta.new_graph.objects
10793        );
10794
10795        ctx.close().await;
10796    }
10797
10798    #[tokio::test(flavor = "multi_thread")]
10799    async fn test_segments_symmetric_arcs() {
10800        let initial_source = "\
10801sketch(on = XY) {
10802  arc(start = [var -15, var 0], end = [var -10, var 5], center = [var -10, var 0])
10803  arc(start = [var 6, var 2], end = [var 12, var -4], center = [var 8, var 1])
10804  line(start = [var 0, var -10], end = [var 0, var 10])
10805}
10806";
10807
10808        let program = Program::parse(initial_source).unwrap().0.unwrap();
10809
10810        let mut frontend = FrontendState::new();
10811
10812        let ctx = ExecutorContext::new_mock(None).await;
10813        let version = Version(0);
10814
10815        frontend.program = program.clone();
10816        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10817        frontend.update_state_after_exec(outcome, true);
10818        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10819        let sketch_id = sketch_object.id;
10820        let sketch = expect_sketch(sketch_object);
10821        let arc1_id = *sketch.segments.get(3).unwrap();
10822        let arc2_id = *sketch.segments.get(7).unwrap();
10823        let axis_id = *sketch.segments.get(10).unwrap();
10824
10825        let constraint = Constraint::Symmetric(Symmetric {
10826            input: vec![arc1_id, arc2_id],
10827            axis: axis_id,
10828        });
10829        let (src_delta, scene_delta) = frontend
10830            .add_constraint(&ctx, version, sketch_id, constraint)
10831            .await
10832            .unwrap();
10833        assert_eq!(
10834            src_delta.text.as_str(),
10835            "\
10836sketch(on = XY) {
10837  arc1 = arc(start = [var -15, var 0], end = [var -10, var 5], center = [var -10, var 0])
10838  arc2 = arc(start = [var 6, var 2], end = [var 12, var -4], center = [var 8, var 1])
10839  line1 = line(start = [var 0, var -10], end = [var 0, var 10])
10840  symmetric([arc1, arc2], axis = line1)
10841}
10842"
10843        );
10844        assert_eq!(
10845            scene_delta.new_graph.objects.len(),
10846            14,
10847            "{:#?}",
10848            scene_delta.new_graph.objects
10849        );
10850
10851        ctx.close().await;
10852    }
10853
10854    #[tokio::test(flavor = "multi_thread")]
10855    async fn test_sketch_on_face_simple() {
10856        let initial_source = "\
10857len = 2mm
10858cube = startSketchOn(XY)
10859  |> startProfile(at = [0, 0])
10860  |> line(end = [len, 0], tag = $side)
10861  |> line(end = [0, len])
10862  |> line(end = [-len, 0])
10863  |> line(end = [0, -len])
10864  |> close()
10865  |> extrude(length = len)
10866
10867face = faceOf(cube, face = side)
10868";
10869
10870        let program = Program::parse(initial_source).unwrap().0.unwrap();
10871
10872        let mut frontend = FrontendState::new();
10873
10874        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10875        let mock_ctx = ExecutorContext::new_mock(None).await;
10876        let version = Version(0);
10877
10878        frontend.hack_set_program(&ctx, program).await.unwrap();
10879        let face_object = find_first_face_object(&frontend.scene_graph).unwrap();
10880        let face_id = face_object.id;
10881
10882        let sketch_args = SketchCtor {
10883            on: Plane::Object(face_id),
10884        };
10885        let (_src_delta, scene_delta, sketch_id) = frontend
10886            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
10887            .await
10888            .unwrap();
10889        assert_eq!(sketch_id, ObjectId(2));
10890        assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
10891        let sketch_object = &scene_delta.new_graph.objects[2];
10892        assert_eq!(sketch_object.id, ObjectId(2));
10893        assert_eq!(
10894            sketch_object.kind,
10895            ObjectKind::Sketch(Sketch {
10896                args: SketchCtor {
10897                    on: Plane::Object(face_id),
10898                },
10899                plane: face_id,
10900                segments: vec![],
10901                constraints: vec![],
10902            })
10903        );
10904        assert_eq!(scene_delta.new_graph.objects.len(), 8);
10905
10906        ctx.close().await;
10907        mock_ctx.close().await;
10908    }
10909
10910    #[tokio::test(flavor = "multi_thread")]
10911    async fn test_sketch_on_wall_artifact_from_region_extrude() {
10912        let initial_source = "\
10913s = sketch(on = YZ) {
10914  line1 = line(start = [0, 0], end = [0, 1])
10915  line2 = line(start = [0, 1], end = [1, 1])
10916  line3 = line(start = [1, 1], end = [0, 0])
10917}
10918region001 = region(point = [0.1, 0.1], sketch = s)
10919extrude001 = extrude(region001, length = 5)
10920";
10921
10922        let program = Program::parse(initial_source).unwrap().0.unwrap();
10923
10924        let mut frontend = FrontendState::new();
10925        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10926        let version = Version(0);
10927
10928        frontend.hack_set_program(&ctx, program).await.unwrap();
10929        let wall_object_id = find_first_wall_object_id(&frontend.scene_graph).expect("expected a wall object");
10930
10931        let sketch_args = SketchCtor {
10932            on: Plane::Object(wall_object_id),
10933        };
10934        let (src_delta, _scene_delta, _sketch_id) = frontend
10935            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
10936            .await
10937            .unwrap();
10938        assert!(src_delta.text.contains("faceOf(extrude001, face = region001.tags."));
10939
10940        ctx.close().await;
10941    }
10942
10943    #[tokio::test(flavor = "multi_thread")]
10944    async fn test_sketch_on_wall_artifact_from_split_region_extrude() {
10945        let initial_source = "\
10946sketch001 = sketch(on = YZ) {
10947  line1 = line(start = [var 0.49, var -0.39], end = [var 6.52, var -0.39])
10948  line2 = line(start = [var 6.52, var -0.39], end = [var 6.52, var 4.9])
10949  line3 = line(start = [var 6.52, var 4.9], end = [var 0.49, var 4.9])
10950  line4 = line(start = [var 0.49, var 4.9], end = [var 0.49, var -0.39])
10951  coincident([line1.end, line2.start])
10952  coincident([line2.end, line3.start])
10953  coincident([line3.end, line4.start])
10954  coincident([line4.end, line1.start])
10955  parallel([line2, line4])
10956  parallel([line3, line1])
10957  perpendicular([line1, line2])
10958  horizontal(line3)
10959  line5 = line(start = [2.35, 6.65], end = [5.89, -2.7])
10960}
10961region001 = region(point = [3.1, 3.74], sketch = sketch001)
10962extrude001 = extrude(region001, length = 5)
10963";
10964
10965        let program = Program::parse(initial_source).unwrap().0.unwrap();
10966
10967        let mut frontend = FrontendState::new();
10968        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10969        let version = Version(0);
10970
10971        frontend.hack_set_program(&ctx, program).await.unwrap();
10972        let wall_object_id = find_first_wall_object_id(&frontend.scene_graph).expect("expected a wall object");
10973
10974        let sketch_args = SketchCtor {
10975            on: Plane::Object(wall_object_id),
10976        };
10977        let (src_delta, _scene_delta, _sketch_id) = frontend
10978            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
10979            .await
10980            .unwrap();
10981        assert!(src_delta.text.contains("faceOf(extrude001, face = region001.tags."));
10982
10983        ctx.close().await;
10984    }
10985
10986    #[tokio::test(flavor = "multi_thread")]
10987    async fn test_sketch_on_plane_incremental() {
10988        let initial_source = "\
10989len = 2mm
10990cube = startSketchOn(XY)
10991  |> startProfile(at = [0, 0])
10992  |> line(end = [len, 0], tag = $side)
10993  |> line(end = [0, len])
10994  |> line(end = [-len, 0])
10995  |> line(end = [0, -len])
10996  |> close()
10997  |> extrude(length = len)
10998
10999plane = planeOf(cube, face = side)
11000";
11001
11002        let program = Program::parse(initial_source).unwrap().0.unwrap();
11003
11004        let mut frontend = FrontendState::new();
11005
11006        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11007        let mock_ctx = ExecutorContext::new_mock(None).await;
11008        let version = Version(0);
11009
11010        frontend.hack_set_program(&ctx, program).await.unwrap();
11011        // Find the last plane since the first plane is the XY plane.
11012        let plane_object = frontend
11013            .scene_graph
11014            .objects
11015            .iter()
11016            .rev()
11017            .find(|object| matches!(&object.kind, ObjectKind::Plane(_)))
11018            .unwrap();
11019        let plane_id = plane_object.id;
11020
11021        let sketch_args = SketchCtor {
11022            on: Plane::Object(plane_id),
11023        };
11024        let (src_delta, scene_delta, sketch_id) = frontend
11025            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
11026            .await
11027            .unwrap();
11028        assert_eq!(
11029            src_delta.text.as_str(),
11030            "\
11031len = 2mm
11032cube = startSketchOn(XY)
11033  |> startProfile(at = [0, 0])
11034  |> line(end = [len, 0], tag = $side)
11035  |> line(end = [0, len])
11036  |> line(end = [-len, 0])
11037  |> line(end = [0, -len])
11038  |> close()
11039  |> extrude(length = len)
11040
11041plane = planeOf(cube, face = side)
11042sketch001 = sketch(on = plane) {
11043}
11044"
11045        );
11046        assert_eq!(sketch_id, ObjectId(2));
11047        assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
11048        let sketch_object = &scene_delta.new_graph.objects[2];
11049        assert_eq!(sketch_object.id, ObjectId(2));
11050        assert_eq!(
11051            sketch_object.kind,
11052            ObjectKind::Sketch(Sketch {
11053                args: SketchCtor {
11054                    on: Plane::Object(plane_id),
11055                },
11056                plane: plane_id,
11057                segments: vec![],
11058                constraints: vec![],
11059            })
11060        );
11061        assert_eq!(scene_delta.new_graph.objects.len(), 9);
11062
11063        let plane_object = scene_delta.new_graph.objects.get(plane_id.0).unwrap();
11064        assert_eq!(plane_object.id, plane_id);
11065        assert_eq!(plane_object.kind, ObjectKind::Plane(Plane::Object(plane_id)));
11066
11067        ctx.close().await;
11068        mock_ctx.close().await;
11069    }
11070
11071    #[tokio::test(flavor = "multi_thread")]
11072    async fn test_new_sketch_uses_unique_variable_name() {
11073        let initial_source = "\
11074sketch1 = sketch(on = XY) {
11075}
11076";
11077
11078        let program = Program::parse(initial_source).unwrap().0.unwrap();
11079
11080        let mut frontend = FrontendState::new();
11081        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11082        let version = Version(0);
11083
11084        frontend.hack_set_program(&ctx, program).await.unwrap();
11085
11086        let sketch_args = SketchCtor {
11087            on: Plane::Default(PlaneName::Yz),
11088        };
11089        let (src_delta, _, _) = frontend
11090            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
11091            .await
11092            .unwrap();
11093
11094        assert_eq!(
11095            src_delta.text.as_str(),
11096            "\
11097sketch1 = sketch(on = XY) {
11098}
11099sketch001 = sketch(on = YZ) {
11100}
11101"
11102        );
11103
11104        ctx.close().await;
11105    }
11106
11107    #[tokio::test(flavor = "multi_thread")]
11108    async fn test_new_sketch_twice_using_same_plane() {
11109        let initial_source = "\
11110sketch1 = sketch(on = XY) {
11111}
11112";
11113
11114        let program = Program::parse(initial_source).unwrap().0.unwrap();
11115
11116        let mut frontend = FrontendState::new();
11117        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11118        let version = Version(0);
11119
11120        frontend.hack_set_program(&ctx, program).await.unwrap();
11121
11122        let sketch_args = SketchCtor {
11123            on: Plane::Default(PlaneName::Xy),
11124        };
11125        let (src_delta, _, _) = frontend
11126            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
11127            .await
11128            .unwrap();
11129
11130        assert_eq!(
11131            src_delta.text.as_str(),
11132            "\
11133sketch1 = sketch(on = XY) {
11134}
11135sketch001 = sketch(on = XY) {
11136}
11137"
11138        );
11139
11140        ctx.close().await;
11141    }
11142
11143    #[tokio::test(flavor = "multi_thread")]
11144    async fn test_sketch_mode_reuses_cached_on_expression() {
11145        let initial_source = "\
11146width = 2mm
11147sketch(on = offsetPlane(XY, offset = width)) {
11148  line1 = line(start = [var 0, var 0], end = [var 1mm, var 0])
11149  distance([line1.start, line1.end]) == width
11150}
11151";
11152        let program = Program::parse(initial_source).unwrap().0.unwrap();
11153
11154        let mut frontend = FrontendState::new();
11155        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11156        let mock_ctx = ExecutorContext::new_mock(None).await;
11157        let version = Version(0);
11158        let project_id = ProjectId(0);
11159        let file_id = FileId(0);
11160
11161        frontend.hack_set_program(&ctx, program).await.unwrap();
11162        let initial_object_count = frontend.scene_graph.objects.len();
11163        let sketch_id = find_first_sketch_object(&frontend.scene_graph)
11164            .expect("Expected sketch object to exist")
11165            .id;
11166
11167        // Entering sketch mode should reuse cached `on` expression state
11168        // (offsetPlane result), not fail or create extra on-surface objects.
11169        let scene_delta = frontend
11170            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
11171            .await
11172            .unwrap();
11173        assert_eq!(scene_delta.new_graph.objects.len(), initial_object_count);
11174
11175        // A follow-up sketch-mode execution should keep the same stable object
11176        // graph shape as well.
11177        let (_src_delta, scene_delta) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
11178        assert_eq!(scene_delta.new_graph.objects.len(), initial_object_count);
11179
11180        ctx.close().await;
11181        mock_ctx.close().await;
11182    }
11183
11184    #[tokio::test(flavor = "multi_thread")]
11185    async fn test_multiple_sketch_blocks() {
11186        let initial_source = "\
11187// Cube that requires the engine.
11188width = 2
11189sketch001 = startSketchOn(XY)
11190profile001 = startProfile(sketch001, at = [0, 0])
11191  |> yLine(length = width, tag = $seg1)
11192  |> xLine(length = width)
11193  |> yLine(length = -width)
11194  |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
11195  |> close()
11196extrude001 = extrude(profile001, length = width)
11197
11198// Get a value that requires the engine.
11199x = segLen(seg1)
11200
11201// Triangle with side length 2*x.
11202sketch(on = XY) {
11203  line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
11204  line2 = line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
11205  coincident([line1.end, line2.start])
11206  line3 = line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
11207  coincident([line2.end, line3.start])
11208  coincident([line3.end, line1.start])
11209  equalLength([line3, line1])
11210  equalLength([line1, line2])
11211  distance([line1.start, line1.end]) == 2*x
11212}
11213
11214// Line segment with length x.
11215sketch2 = sketch(on = XY) {
11216  line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
11217  distance([line1.start, line1.end]) == x
11218}
11219";
11220
11221        let program = Program::parse(initial_source).unwrap().0.unwrap();
11222
11223        let mut frontend = FrontendState::new();
11224
11225        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11226        let mock_ctx = ExecutorContext::new_mock(None).await;
11227        let version = Version(0);
11228        let project_id = ProjectId(0);
11229        let file_id = FileId(0);
11230
11231        frontend.hack_set_program(&ctx, program).await.unwrap();
11232        let sketch_objects = frontend
11233            .scene_graph
11234            .objects
11235            .iter()
11236            .filter(|obj| matches!(obj.kind, ObjectKind::Sketch(_)))
11237            .collect::<Vec<_>>();
11238        let sketch1_id = sketch_objects.first().unwrap().id;
11239        let sketch2_id = sketch_objects.get(1).unwrap().id;
11240        // First point in sketch1.
11241        let point1_id = ObjectId(sketch1_id.0 + 1);
11242        // First point in sketch2.
11243        let point2_id = ObjectId(sketch2_id.0 + 1);
11244
11245        // Edit the first sketch. Objects before the sketch block should be
11246        // present from execution cache so that we can sketch on prior planes,
11247        // for example. Objects after the first sketch block should not be
11248        // present since those statements are skipped in sketch mode.
11249        //
11250        // - startSketchOn(XY) Plane 1
11251        // - sketch on=XY Plane 1
11252        // - Sketch block 16
11253        let scene_delta = frontend
11254            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch1_id)
11255            .await
11256            .unwrap();
11257        assert_eq!(
11258            scene_delta.new_graph.objects.len(),
11259            18,
11260            "{:#?}",
11261            scene_delta.new_graph.objects
11262        );
11263
11264        // Edit a point in the first sketch.
11265        let point_ctor = PointCtor {
11266            position: Point2d {
11267                x: Expr::Var(Number {
11268                    value: 1.0,
11269                    units: NumericSuffix::Mm,
11270                }),
11271                y: Expr::Var(Number {
11272                    value: 2.0,
11273                    units: NumericSuffix::Mm,
11274                }),
11275            },
11276        };
11277        let segments = vec![ExistingSegmentCtor {
11278            id: point1_id,
11279            ctor: SegmentCtor::Point(point_ctor),
11280        }];
11281        let (src_delta, _) = frontend
11282            .edit_segments(&mock_ctx, version, sketch1_id, segments)
11283            .await
11284            .unwrap();
11285        // Only the first sketch block changes.
11286        assert_eq!(
11287            src_delta.text.as_str(),
11288            "\
11289// Cube that requires the engine.
11290width = 2
11291sketch001 = startSketchOn(XY)
11292profile001 = startProfile(sketch001, at = [0, 0])
11293  |> yLine(length = width, tag = $seg1)
11294  |> xLine(length = width)
11295  |> yLine(length = -width)
11296  |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
11297  |> close()
11298extrude001 = extrude(profile001, length = width)
11299
11300// Get a value that requires the engine.
11301x = segLen(seg1)
11302
11303// Triangle with side length 2*x.
11304sketch(on = XY) {
11305  line1 = line(start = [var 1mm, var 2mm], end = [var 2.32mm, var -1.78mm])
11306  line2 = line(start = [var 2.32mm, var -1.78mm], end = [var -1.61mm, var -1.03mm])
11307  coincident([line1.end, line2.start])
11308  line3 = line(start = [var -1.61mm, var -1.03mm], end = [var 1mm, var 2mm])
11309  coincident([line2.end, line3.start])
11310  coincident([line3.end, line1.start])
11311  equalLength([line3, line1])
11312  equalLength([line1, line2])
11313  distance([line1.start, line1.end]) == 2 * x
11314}
11315
11316// Line segment with length x.
11317sketch2 = sketch(on = XY) {
11318  line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
11319  distance([line1.start, line1.end]) == x
11320}
11321"
11322        );
11323
11324        // Execute mock to simulate drag end.
11325        let (src_delta, _) = frontend.execute_mock(&mock_ctx, version, sketch1_id).await.unwrap();
11326        // Only the first sketch block changes.
11327        assert_eq!(
11328            src_delta.text.as_str(),
11329            "\
11330// Cube that requires the engine.
11331width = 2
11332sketch001 = startSketchOn(XY)
11333profile001 = startProfile(sketch001, at = [0, 0])
11334  |> yLine(length = width, tag = $seg1)
11335  |> xLine(length = width)
11336  |> yLine(length = -width)
11337  |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
11338  |> close()
11339extrude001 = extrude(profile001, length = width)
11340
11341// Get a value that requires the engine.
11342x = segLen(seg1)
11343
11344// Triangle with side length 2*x.
11345sketch(on = XY) {
11346  line1 = line(start = [var 1mm, var 2mm], end = [var 1.28mm, var -0.78mm])
11347  line2 = line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
11348  coincident([line1.end, line2.start])
11349  line3 = line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
11350  coincident([line2.end, line3.start])
11351  coincident([line3.end, line1.start])
11352  equalLength([line3, line1])
11353  equalLength([line1, line2])
11354  distance([line1.start, line1.end]) == 2 * x
11355}
11356
11357// Line segment with length x.
11358sketch2 = sketch(on = XY) {
11359  line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
11360  distance([line1.start, line1.end]) == x
11361}
11362"
11363        );
11364        // Exit sketch. Objects from the entire program should be present.
11365        //
11366        // - startSketchOn(XY) Plane 1
11367        // - sketch on=XY Plane 1
11368        // - Sketch block 16
11369        // - sketch on=XY cached
11370        // - Sketch block 5
11371        let scene = frontend.exit_sketch(&ctx, version, sketch1_id).await.unwrap();
11372        assert_eq!(scene.objects.len(), 30, "{:#?}", scene.objects);
11373
11374        // Edit the second sketch.
11375        //
11376        // - startSketchOn(XY) Plane 1
11377        // - sketch on=XY Plane 1
11378        // - Sketch block 16
11379        // - sketch on=XY cached
11380        // - Sketch block 5
11381        let scene_delta = frontend
11382            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch2_id)
11383            .await
11384            .unwrap();
11385        assert_eq!(
11386            scene_delta.new_graph.objects.len(),
11387            24,
11388            "{:#?}",
11389            scene_delta.new_graph.objects
11390        );
11391
11392        // Edit a point in the second sketch.
11393        let point_ctor = PointCtor {
11394            position: Point2d {
11395                x: Expr::Var(Number {
11396                    value: 3.0,
11397                    units: NumericSuffix::Mm,
11398                }),
11399                y: Expr::Var(Number {
11400                    value: 4.0,
11401                    units: NumericSuffix::Mm,
11402                }),
11403            },
11404        };
11405        let segments = vec![ExistingSegmentCtor {
11406            id: point2_id,
11407            ctor: SegmentCtor::Point(point_ctor),
11408        }];
11409        let (src_delta, _) = frontend
11410            .edit_segments(&mock_ctx, version, sketch2_id, segments)
11411            .await
11412            .unwrap();
11413        // Only the second sketch block changes.
11414        assert_eq!(
11415            src_delta.text.as_str(),
11416            "\
11417// Cube that requires the engine.
11418width = 2
11419sketch001 = startSketchOn(XY)
11420profile001 = startProfile(sketch001, at = [0, 0])
11421  |> yLine(length = width, tag = $seg1)
11422  |> xLine(length = width)
11423  |> yLine(length = -width)
11424  |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
11425  |> close()
11426extrude001 = extrude(profile001, length = width)
11427
11428// Get a value that requires the engine.
11429x = segLen(seg1)
11430
11431// Triangle with side length 2*x.
11432sketch(on = XY) {
11433  line1 = line(start = [var 1mm, var 2mm], end = [var 1.28mm, var -0.78mm])
11434  line2 = line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
11435  coincident([line1.end, line2.start])
11436  line3 = line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
11437  coincident([line2.end, line3.start])
11438  coincident([line3.end, line1.start])
11439  equalLength([line3, line1])
11440  equalLength([line1, line2])
11441  distance([line1.start, line1.end]) == 2 * x
11442}
11443
11444// Line segment with length x.
11445sketch2 = sketch(on = XY) {
11446  line1 = line(start = [var 3mm, var 4mm], end = [var 2.32mm, var 2.12mm])
11447  distance([line1.start, line1.end]) == x
11448}
11449"
11450        );
11451
11452        // Execute mock to simulate drag end.
11453        let (src_delta, _) = frontend.execute_mock(&mock_ctx, version, sketch2_id).await.unwrap();
11454        // Only the second sketch block changes.
11455        assert_eq!(
11456            src_delta.text.as_str(),
11457            "\
11458// Cube that requires the engine.
11459width = 2
11460sketch001 = startSketchOn(XY)
11461profile001 = startProfile(sketch001, at = [0, 0])
11462  |> yLine(length = width, tag = $seg1)
11463  |> xLine(length = width)
11464  |> yLine(length = -width)
11465  |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
11466  |> close()
11467extrude001 = extrude(profile001, length = width)
11468
11469// Get a value that requires the engine.
11470x = segLen(seg1)
11471
11472// Triangle with side length 2*x.
11473sketch(on = XY) {
11474  line1 = line(start = [var 1mm, var 2mm], end = [var 1.28mm, var -0.78mm])
11475  line2 = line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
11476  coincident([line1.end, line2.start])
11477  line3 = line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
11478  coincident([line2.end, line3.start])
11479  coincident([line3.end, line1.start])
11480  equalLength([line3, line1])
11481  equalLength([line1, line2])
11482  distance([line1.start, line1.end]) == 2 * x
11483}
11484
11485// Line segment with length x.
11486sketch2 = sketch(on = XY) {
11487  line1 = line(start = [var 3mm, var 4mm], end = [var 1.28mm, var -0.78mm])
11488  distance([line1.start, line1.end]) == x
11489}
11490"
11491        );
11492
11493        ctx.close().await;
11494        mock_ctx.close().await;
11495    }
11496
11497    #[tokio::test(flavor = "multi_thread")]
11498    async fn test_exit_sketch_without_changes_allows_entering_next_sketch() {
11499        clear_mem_cache().await;
11500
11501        let source = r#"sketch001 = sketch(on = XZ) {
11502  circle1 = circle(start = [var -1.96mm, var 2.77mm], center = [var -2.69mm, var 3.44mm])
11503}
11504sketch002 = sketch(on = XY) {
11505  line1 = line(start = [var 0mm, var 0mm], end = [var 4.68mm, var 0mm])
11506  line2 = line(start = [var 4.68mm, var 0mm], end = [var 4.68mm, var 2.96mm])
11507  line3 = line(start = [var 4.68mm, var 2.96mm], end = [var 0mm, var 2.96mm])
11508  line4 = line(start = [var 0mm, var 2.96mm], end = [var 0mm, var 0mm])
11509  coincident([line1.end, line2.start])
11510  coincident([line2.end, line3.start])
11511  coincident([line3.end, line4.start])
11512  coincident([line4.end, line1.start])
11513  parallel([line2, line4])
11514  parallel([line3, line1])
11515  perpendicular([line1, line2])
11516  horizontal(line3)
11517  coincident([line1.start, ORIGIN])
11518}
11519"#;
11520
11521        let program = Program::parse(source).unwrap().0.unwrap();
11522        let mut frontend = FrontendState::new();
11523        let ctx = ExecutorContext::new_with_engine(
11524            std::sync::Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().unwrap())),
11525            Default::default(),
11526        );
11527        let mock_ctx = ExecutorContext::new_mock(None).await;
11528        let version = Version(0);
11529        let project_id = ProjectId(0);
11530        let file_id = FileId(0);
11531
11532        frontend.hack_set_program(&ctx, program).await.unwrap();
11533        let sketch_objects = frontend
11534            .scene_graph
11535            .objects
11536            .iter()
11537            .filter(|object| matches!(object.kind, ObjectKind::Sketch(_)))
11538            .collect::<Vec<_>>();
11539        assert_eq!(sketch_objects.len(), 2, "{:#?}", frontend.scene_graph.objects);
11540
11541        let sketch1_id = sketch_objects[0].id;
11542        let sketch2_id = sketch_objects[1].id;
11543
11544        frontend
11545            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch1_id)
11546            .await
11547            .unwrap();
11548        frontend.exit_sketch(&ctx, version, sketch1_id).await.unwrap();
11549
11550        let scene_delta = frontend
11551            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch2_id)
11552            .await
11553            .unwrap();
11554        assert_eq!(scene_delta.new_graph.sketch_mode, Some(sketch2_id));
11555
11556        clear_mem_cache().await;
11557        ctx.close().await;
11558        mock_ctx.close().await;
11559    }
11560
11561    // Regression tests: operations on source code with extra whitespace/newlines.
11562    // These test that NodePath-based lookups work correctly when source ranges
11563    // are shifted by extra whitespace that wouldn't be present after formatting.
11564
11565    #[tokio::test(flavor = "multi_thread")]
11566    async fn test_extra_newlines_after_settings_edit_sketch_add_point() {
11567        // Extra newlines after @settings line - this shifts all source ranges.
11568        let initial_source = "@settings(defaultLengthUnit = mm)
11569
11570
11571
11572sketch001 = sketch(on = XY) {
11573  point(at = [1in, 2in])
11574}
11575";
11576
11577        let program = Program::parse(initial_source).unwrap().0.unwrap();
11578        let mut frontend = FrontendState::new();
11579
11580        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11581        let mock_ctx = ExecutorContext::new_mock(None).await;
11582        let version = Version(0);
11583        let project_id = ProjectId(0);
11584        let file_id = FileId(0);
11585
11586        frontend.hack_set_program(&ctx, program).await.unwrap();
11587        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11588        let sketch_id = sketch_object.id;
11589
11590        // Edit sketch should succeed despite extra newlines.
11591        frontend
11592            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
11593            .await
11594            .unwrap();
11595
11596        // Add a new point to the sketch.
11597        let point_ctor = PointCtor {
11598            position: Point2d {
11599                x: Expr::Number(Number {
11600                    value: 5.0,
11601                    units: NumericSuffix::Mm,
11602                }),
11603                y: Expr::Number(Number {
11604                    value: 6.0,
11605                    units: NumericSuffix::Mm,
11606                }),
11607            },
11608        };
11609        let segment = SegmentCtor::Point(point_ctor);
11610        let (src_delta, scene_delta) = frontend
11611            .add_segment(&mock_ctx, version, sketch_id, segment, None)
11612            .await
11613            .unwrap();
11614        // After adding a point, the source should be reformatted with standard whitespace.
11615        assert!(
11616            src_delta.text.contains("point(at = [5mm, 6mm])"),
11617            "Expected new point in source, got: {}",
11618            src_delta.text
11619        );
11620        assert!(!scene_delta.new_objects.is_empty());
11621
11622        ctx.close().await;
11623        mock_ctx.close().await;
11624    }
11625
11626    #[tokio::test(flavor = "multi_thread")]
11627    async fn test_extra_newlines_after_settings_add_line_to_empty_sketch() {
11628        // Extra newlines after @settings, with an empty sketch block.
11629        let initial_source = "@settings(defaultLengthUnit = mm)
11630
11631
11632
11633s = sketch(on = XY) {}
11634";
11635
11636        let program = Program::parse(initial_source).unwrap().0.unwrap();
11637        let mut frontend = FrontendState::new();
11638
11639        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11640        let mock_ctx = ExecutorContext::new_mock(None).await;
11641        let version = Version(0);
11642
11643        frontend.hack_set_program(&ctx, program).await.unwrap();
11644        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11645        let sketch_id = sketch_object.id;
11646
11647        let line_ctor = LineCtor {
11648            start: Point2d {
11649                x: Expr::Number(Number {
11650                    value: 0.0,
11651                    units: NumericSuffix::Mm,
11652                }),
11653                y: Expr::Number(Number {
11654                    value: 0.0,
11655                    units: NumericSuffix::Mm,
11656                }),
11657            },
11658            end: Point2d {
11659                x: Expr::Number(Number {
11660                    value: 10.0,
11661                    units: NumericSuffix::Mm,
11662                }),
11663                y: Expr::Number(Number {
11664                    value: 10.0,
11665                    units: NumericSuffix::Mm,
11666                }),
11667            },
11668            construction: None,
11669        };
11670        let segment = SegmentCtor::Line(line_ctor);
11671        let (src_delta, scene_delta) = frontend
11672            .add_segment(&mock_ctx, version, sketch_id, segment, None)
11673            .await
11674            .unwrap();
11675        assert!(
11676            src_delta.text.contains("line(start = [0mm, 0mm], end = [10mm, 10mm])"),
11677            "Expected line in source, got: {}",
11678            src_delta.text
11679        );
11680        // Line creates start point, end point, and line segment.
11681        assert_eq!(scene_delta.new_objects.len(), 3);
11682
11683        ctx.close().await;
11684        mock_ctx.close().await;
11685    }
11686
11687    #[tokio::test(flavor = "multi_thread")]
11688    async fn test_extra_newlines_between_operations_edit_line() {
11689        // Extra newlines between @settings and sketch, and inside the sketch block.
11690        let initial_source = "@settings(defaultLengthUnit = mm)
11691
11692
11693sketch001 = sketch(on = XY) {
11694
11695  line1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm])
11696
11697}
11698";
11699
11700        let program = Program::parse(initial_source).unwrap().0.unwrap();
11701        let mut frontend = FrontendState::new();
11702
11703        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11704        let mock_ctx = ExecutorContext::new_mock(None).await;
11705        let version = Version(0);
11706        let project_id = ProjectId(0);
11707        let file_id = FileId(0);
11708
11709        frontend.hack_set_program(&ctx, program).await.unwrap();
11710        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11711        let sketch_id = sketch_object.id;
11712        let sketch = expect_sketch(sketch_object);
11713
11714        // Extract segment IDs before edit_sketch borrows frontend mutably.
11715        let line_id = sketch
11716            .segments
11717            .iter()
11718            .copied()
11719            .find(|seg_id| {
11720                matches!(
11721                    &frontend.scene_graph.objects[seg_id.0].kind,
11722                    ObjectKind::Segment {
11723                        segment: Segment::Line(_)
11724                    }
11725                )
11726            })
11727            .expect("Expected a line segment in sketch");
11728
11729        // Enter sketch edit mode.
11730        frontend
11731            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
11732            .await
11733            .unwrap();
11734
11735        // Edit the line.
11736        let line_ctor = LineCtor {
11737            start: Point2d {
11738                x: Expr::Var(Number {
11739                    value: 1.0,
11740                    units: NumericSuffix::Mm,
11741                }),
11742                y: Expr::Var(Number {
11743                    value: 2.0,
11744                    units: NumericSuffix::Mm,
11745                }),
11746            },
11747            end: Point2d {
11748                x: Expr::Var(Number {
11749                    value: 13.0,
11750                    units: NumericSuffix::Mm,
11751                }),
11752                y: Expr::Var(Number {
11753                    value: 14.0,
11754                    units: NumericSuffix::Mm,
11755                }),
11756            },
11757            construction: None,
11758        };
11759        let segments = vec![ExistingSegmentCtor {
11760            id: line_id,
11761            ctor: SegmentCtor::Line(line_ctor),
11762        }];
11763        let (src_delta, _scene_delta) = frontend
11764            .edit_segments(&mock_ctx, version, sketch_id, segments)
11765            .await
11766            .unwrap();
11767        assert!(
11768            src_delta
11769                .text
11770                .contains("line(start = [var 1mm, var 2mm], end = [var 13mm, var 14mm])"),
11771            "Expected edited line in source, got: {}",
11772            src_delta.text
11773        );
11774
11775        ctx.close().await;
11776        mock_ctx.close().await;
11777    }
11778
11779    #[tokio::test(flavor = "multi_thread")]
11780    async fn test_extra_newlines_delete_segment() {
11781        // Extra whitespace before and after the sketch block.
11782        let initial_source = "@settings(defaultLengthUnit = mm)
11783
11784
11785
11786sketch001 = sketch(on = XY) {
11787  circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
11788}
11789";
11790
11791        let program = Program::parse(initial_source).unwrap().0.unwrap();
11792        let mut frontend = FrontendState::new();
11793
11794        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11795        let mock_ctx = ExecutorContext::new_mock(None).await;
11796        let version = Version(0);
11797
11798        frontend.hack_set_program(&ctx, program).await.unwrap();
11799        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11800        let sketch_id = sketch_object.id;
11801        let sketch = expect_sketch(sketch_object);
11802
11803        // The sketch should have 3 segments: start point, center point, and the circle.
11804        assert_eq!(sketch.segments.len(), 3);
11805        let circle_id = sketch.segments[2];
11806
11807        // Delete the circle despite extra newlines in original source.
11808        let (src_delta, scene_delta) = frontend
11809            .delete_objects(&mock_ctx, version, sketch_id, vec![], vec![circle_id])
11810            .await
11811            .unwrap();
11812        assert!(
11813            src_delta.text.contains("sketch(on = XY) {"),
11814            "Expected sketch block in source, got: {}",
11815            src_delta.text
11816        );
11817        let new_sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
11818        let new_sketch = expect_sketch(new_sketch_object);
11819        assert_eq!(new_sketch.segments.len(), 0);
11820
11821        ctx.close().await;
11822        mock_ctx.close().await;
11823    }
11824
11825    #[tokio::test(flavor = "multi_thread")]
11826    async fn test_unformatted_source_add_arc() {
11827        // Source with inconsistent whitespace - tabs, extra spaces, multiple blank lines.
11828        let initial_source = "@settings(defaultLengthUnit = mm)
11829
11830
11831
11832
11833sketch001 = sketch(on = XY) {
11834}
11835";
11836
11837        let program = Program::parse(initial_source).unwrap().0.unwrap();
11838        let mut frontend = FrontendState::new();
11839
11840        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11841        let mock_ctx = ExecutorContext::new_mock(None).await;
11842        let version = Version(0);
11843
11844        frontend.hack_set_program(&ctx, program).await.unwrap();
11845        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11846        let sketch_id = sketch_object.id;
11847
11848        let arc_ctor = ArcCtor {
11849            start: Point2d {
11850                x: Expr::Var(Number {
11851                    value: 5.0,
11852                    units: NumericSuffix::Mm,
11853                }),
11854                y: Expr::Var(Number {
11855                    value: 0.0,
11856                    units: NumericSuffix::Mm,
11857                }),
11858            },
11859            end: Point2d {
11860                x: Expr::Var(Number {
11861                    value: 0.0,
11862                    units: NumericSuffix::Mm,
11863                }),
11864                y: Expr::Var(Number {
11865                    value: 5.0,
11866                    units: NumericSuffix::Mm,
11867                }),
11868            },
11869            center: Point2d {
11870                x: Expr::Var(Number {
11871                    value: 0.0,
11872                    units: NumericSuffix::Mm,
11873                }),
11874                y: Expr::Var(Number {
11875                    value: 0.0,
11876                    units: NumericSuffix::Mm,
11877                }),
11878            },
11879            construction: None,
11880        };
11881        let segment = SegmentCtor::Arc(arc_ctor);
11882        let (src_delta, scene_delta) = frontend
11883            .add_segment(&mock_ctx, version, sketch_id, segment, None)
11884            .await
11885            .unwrap();
11886        assert!(
11887            src_delta
11888                .text
11889                .contains("arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])"),
11890            "Expected arc in source, got: {}",
11891            src_delta.text
11892        );
11893        assert!(!scene_delta.new_objects.is_empty());
11894
11895        ctx.close().await;
11896        mock_ctx.close().await;
11897    }
11898
11899    #[tokio::test(flavor = "multi_thread")]
11900    async fn test_extra_newlines_add_circle() {
11901        // Extra blank lines between settings and sketch.
11902        let initial_source = "@settings(defaultLengthUnit = mm)
11903
11904
11905
11906sketch001 = sketch(on = XY) {
11907}
11908";
11909
11910        let program = Program::parse(initial_source).unwrap().0.unwrap();
11911        let mut frontend = FrontendState::new();
11912
11913        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11914        let mock_ctx = ExecutorContext::new_mock(None).await;
11915        let version = Version(0);
11916
11917        frontend.hack_set_program(&ctx, program).await.unwrap();
11918        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11919        let sketch_id = sketch_object.id;
11920
11921        let circle_ctor = CircleCtor {
11922            start: Point2d {
11923                x: Expr::Var(Number {
11924                    value: 5.0,
11925                    units: NumericSuffix::Mm,
11926                }),
11927                y: Expr::Var(Number {
11928                    value: 0.0,
11929                    units: NumericSuffix::Mm,
11930                }),
11931            },
11932            center: Point2d {
11933                x: Expr::Var(Number {
11934                    value: 0.0,
11935                    units: NumericSuffix::Mm,
11936                }),
11937                y: Expr::Var(Number {
11938                    value: 0.0,
11939                    units: NumericSuffix::Mm,
11940                }),
11941            },
11942            construction: None,
11943        };
11944        let segment = SegmentCtor::Circle(circle_ctor);
11945        let (src_delta, scene_delta) = frontend
11946            .add_segment(&mock_ctx, version, sketch_id, segment, None)
11947            .await
11948            .unwrap();
11949        assert!(
11950            src_delta
11951                .text
11952                .contains("circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])"),
11953            "Expected circle in source, got: {}",
11954            src_delta.text
11955        );
11956        assert!(!scene_delta.new_objects.is_empty());
11957
11958        ctx.close().await;
11959        mock_ctx.close().await;
11960    }
11961
11962    #[tokio::test(flavor = "multi_thread")]
11963    async fn test_extra_newlines_add_constraint() {
11964        // Extra newlines with a sketch containing two lines - add a coincident constraint.
11965        let initial_source = "@settings(defaultLengthUnit = mm)
11966
11967
11968
11969sketch001 = sketch(on = XY) {
11970  line1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm])
11971  line2 = line(start = [var 10mm, var 10mm], end = [var 20mm, var 0mm])
11972}
11973";
11974
11975        let program = Program::parse(initial_source).unwrap().0.unwrap();
11976        let mut frontend = FrontendState::new();
11977
11978        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11979        let mock_ctx = ExecutorContext::new_mock(None).await;
11980        let version = Version(0);
11981        let project_id = ProjectId(0);
11982        let file_id = FileId(0);
11983
11984        frontend.hack_set_program(&ctx, program).await.unwrap();
11985        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11986        let sketch_id = sketch_object.id;
11987        let sketch = expect_sketch(sketch_object);
11988
11989        // Extract segment data before edit_sketch borrows frontend mutably.
11990        let line_ids: Vec<ObjectId> = sketch
11991            .segments
11992            .iter()
11993            .copied()
11994            .filter(|seg_id| {
11995                matches!(
11996                    &frontend.scene_graph.objects[seg_id.0].kind,
11997                    ObjectKind::Segment {
11998                        segment: Segment::Line(_)
11999                    }
12000                )
12001            })
12002            .collect();
12003        assert_eq!(line_ids.len(), 2, "Expected two line segments");
12004
12005        let line1 = &frontend.scene_graph.objects[line_ids[0].0];
12006        let ObjectKind::Segment {
12007            segment: Segment::Line(line1_data),
12008        } = &line1.kind
12009        else {
12010            panic!("Expected line");
12011        };
12012        let line2 = &frontend.scene_graph.objects[line_ids[1].0];
12013        let ObjectKind::Segment {
12014            segment: Segment::Line(line2_data),
12015        } = &line2.kind
12016        else {
12017            panic!("Expected line");
12018        };
12019
12020        // Build constraint before entering sketch mode.
12021        let constraint = Constraint::Coincident(Coincident {
12022            segments: vec![line1_data.end.into(), line2_data.start.into()],
12023        });
12024
12025        // Enter sketch edit mode.
12026        frontend
12027            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
12028            .await
12029            .unwrap();
12030        let (src_delta, _scene_delta) = frontend
12031            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12032            .await
12033            .unwrap();
12034        assert!(
12035            src_delta.text.contains("coincident("),
12036            "Expected coincident constraint in source, got: {}",
12037            src_delta.text
12038        );
12039
12040        ctx.close().await;
12041        mock_ctx.close().await;
12042    }
12043
12044    #[tokio::test(flavor = "multi_thread")]
12045    async fn test_extra_newlines_add_line_then_edit_line() {
12046        // Extra newlines after @settings - add a line, then edit it.
12047        let initial_source = "@settings(defaultLengthUnit = mm)
12048
12049
12050
12051sketch001 = sketch(on = XY) {
12052}
12053";
12054
12055        let program = Program::parse(initial_source).unwrap().0.unwrap();
12056        let mut frontend = FrontendState::new();
12057
12058        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12059        let mock_ctx = ExecutorContext::new_mock(None).await;
12060        let version = Version(0);
12061
12062        frontend.hack_set_program(&ctx, program).await.unwrap();
12063        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12064        let sketch_id = sketch_object.id;
12065
12066        // Add a line.
12067        let line_ctor = LineCtor {
12068            start: Point2d {
12069                x: Expr::Number(Number {
12070                    value: 0.0,
12071                    units: NumericSuffix::Mm,
12072                }),
12073                y: Expr::Number(Number {
12074                    value: 0.0,
12075                    units: NumericSuffix::Mm,
12076                }),
12077            },
12078            end: Point2d {
12079                x: Expr::Number(Number {
12080                    value: 10.0,
12081                    units: NumericSuffix::Mm,
12082                }),
12083                y: Expr::Number(Number {
12084                    value: 10.0,
12085                    units: NumericSuffix::Mm,
12086                }),
12087            },
12088            construction: None,
12089        };
12090        let segment = SegmentCtor::Line(line_ctor);
12091        let (src_delta, scene_delta) = frontend
12092            .add_segment(&mock_ctx, version, sketch_id, segment, None)
12093            .await
12094            .unwrap();
12095        assert!(
12096            src_delta.text.contains("line(start = [0mm, 0mm], end = [10mm, 10mm])"),
12097            "Expected line in source after add, got: {}",
12098            src_delta.text
12099        );
12100        // Line creates start point, end point, and line segment.
12101        let line_id = *scene_delta.new_objects.last().unwrap();
12102
12103        // Edit the line.
12104        let line_ctor = LineCtor {
12105            start: Point2d {
12106                x: Expr::Number(Number {
12107                    value: 1.0,
12108                    units: NumericSuffix::Mm,
12109                }),
12110                y: Expr::Number(Number {
12111                    value: 2.0,
12112                    units: NumericSuffix::Mm,
12113                }),
12114            },
12115            end: Point2d {
12116                x: Expr::Number(Number {
12117                    value: 13.0,
12118                    units: NumericSuffix::Mm,
12119                }),
12120                y: Expr::Number(Number {
12121                    value: 14.0,
12122                    units: NumericSuffix::Mm,
12123                }),
12124            },
12125            construction: None,
12126        };
12127        let segments = vec![ExistingSegmentCtor {
12128            id: line_id,
12129            ctor: SegmentCtor::Line(line_ctor),
12130        }];
12131        let (src_delta, scene_delta) = frontend
12132            .edit_segments(&mock_ctx, version, sketch_id, segments)
12133            .await
12134            .unwrap();
12135        assert!(
12136            src_delta.text.contains("line(start = [1mm, 2mm], end = [13mm, 14mm])"),
12137            "Expected edited line in source, got: {}",
12138            src_delta.text
12139        );
12140        assert_eq!(scene_delta.new_objects, vec![]);
12141
12142        ctx.close().await;
12143        mock_ctx.close().await;
12144    }
12145}