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