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