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_api::UnitLength;
9use kcl_error::CompilationIssue;
10use kcl_error::SourceRange;
11use serde::Serialize;
12use uuid::Uuid;
13
14use crate::ExecOutcome;
15use crate::ExecutorContext;
16use crate::KclError;
17use crate::KclErrorWithOutputs;
18use crate::KclValueView;
19use crate::Program;
20use crate::SegmentDragAnchor;
21use crate::collections::AhashIndexSet;
22use crate::execution::Artifact;
23use crate::execution::ArtifactGraph;
24use crate::execution::ArtifactId;
25use crate::execution::CapSubType;
26use crate::execution::CodeRef;
27use crate::execution::MockConfig;
28use crate::execution::SKETCH_BLOCK_PARAM_ON;
29use crate::execution::annotations::WarningLevel;
30use crate::execution::cache::SketchModeState;
31use crate::execution::cache::clear_mem_cache;
32use crate::execution::cache::read_old_memory;
33use crate::execution::cache::write_old_memory;
34use crate::execution::types::adjust_length;
35use crate::fmt::format_number_literal;
36use crate::front::Angle;
37use crate::front::ArcCtor;
38use crate::front::CircleCtor;
39use crate::front::ControlPointSplineCtor;
40use crate::front::Distance;
41use crate::front::EqualRadius;
42use crate::front::Error;
43use crate::front::ExecResult;
44use crate::front::FixedPoint;
45use crate::front::Freedom;
46use crate::front::LinesEqualLength;
47use crate::front::Midpoint;
48use crate::front::Object;
49use crate::front::Parallel;
50use crate::front::Perpendicular;
51use crate::front::PointCtor;
52use crate::front::Symmetric;
53use crate::front::Tangent;
54use crate::frontend::api::CapSource;
55use crate::frontend::api::Expr;
56use crate::frontend::api::FileId;
57use crate::frontend::api::Number;
58use crate::frontend::api::ObjectId;
59use crate::frontend::api::ObjectKind;
60use crate::frontend::api::Plane;
61use crate::frontend::api::ProjectId;
62use crate::frontend::api::RestoreSketchCheckpointOutcome;
63use crate::frontend::api::SceneGraph;
64use crate::frontend::api::SceneGraphDelta;
65use crate::frontend::api::SketchCheckpointId;
66use crate::frontend::api::SourceDelta;
67use crate::frontend::api::SourceRef;
68use crate::frontend::api::SourceRefRange;
69use crate::frontend::api::Version;
70use crate::frontend::api::WallSource;
71use crate::frontend::modify::find_defined_names;
72use crate::frontend::modify::next_free_name;
73use crate::frontend::modify::next_free_name_with_padding;
74use crate::frontend::sketch::Coincident;
75use crate::frontend::sketch::Constraint;
76use crate::frontend::sketch::ConstraintLabelPositionEdit;
77use crate::frontend::sketch::ConstraintSegment;
78use crate::frontend::sketch::Diameter;
79use crate::frontend::sketch::ExistingSegmentCtor;
80use crate::frontend::sketch::Horizontal;
81use crate::frontend::sketch::LineCtor;
82use crate::frontend::sketch::Point2d;
83use crate::frontend::sketch::Radius;
84use crate::frontend::sketch::Segment;
85use crate::frontend::sketch::SegmentCtor;
86use crate::frontend::sketch::SketchApi;
87use crate::frontend::sketch::SketchCtor;
88use crate::frontend::sketch::Vertical;
89use crate::frontend::traverse::MutateBodyItem;
90use crate::frontend::traverse::TraversalReturn;
91use crate::frontend::traverse::Visitor;
92use crate::frontend::traverse::dfs_mut;
93use crate::id::IncIdGenerator;
94use crate::parsing::ast::types as ast;
95use crate::parsing::ast::types::NodePathExt;
96use crate::pretty::NumericSuffix;
97use crate::std::constraints::LinesAtAngleKind;
98use crate::walk::NodeMut;
99use crate::walk::Visitable;
100
101pub(crate) mod api;
102pub(crate) mod modify;
103pub(crate) mod sketch;
104
105pub const MAX_SKETCH_CHECKPOINTS: usize = 100;
106
107#[derive(Debug, Clone)]
108struct SketchCheckpoint {
109 id: SketchCheckpointId,
110 source: SourceDelta,
111 program: Program,
112 scene_graph: SceneGraph,
113 exec_outcome: ExecOutcome,
114 point_freedom_cache: HashMap<ObjectId, Freedom>,
115 mock_memory: Option<SketchModeState>,
116}
117mod traverse;
118pub(crate) mod trim;
119
120struct ArcSizeConstraintParams {
121 points: Vec<ObjectId>,
122 function_name: &'static str,
123 value: f64,
124 units: NumericSuffix,
125 label_position: Option<Point2d<Number>>,
126 constraint_type_name: &'static str,
127}
128
129const POINT_FN: &str = "point";
130const POINT_AT_PARAM: &str = "at";
131const LINE_FN: &str = "line";
132const LINE_VARIABLE: &str = "line";
133const LINE_START_PARAM: &str = "start";
134const LINE_END_PARAM: &str = "end";
135const ARC_FN: &str = "arc";
136const ARC_VARIABLE: &str = "arc";
137const ARC_START_PARAM: &str = "start";
138const ARC_END_PARAM: &str = "end";
139const ARC_CENTER_PARAM: &str = "center";
140const CIRCLE_FN: &str = "circle";
141const CIRCLE_VARIABLE: &str = "circle";
142const CIRCLE_START_PARAM: &str = "start";
143const CIRCLE_CENTER_PARAM: &str = "center";
144const CONTROL_POINT_SPLINE_FN: &str = "controlPointSpline";
145const CONTROL_POINT_SPLINE_POINTS_PARAM: &str = "points";
146const LABEL_POSITION_PARAM: &str = "labelPosition";
147
148const COINCIDENT_FN: &str = "coincident";
149const DIAMETER_FN: &str = "diameter";
150const DISTANCE_FN: &str = "distance";
151const FIXED_FN: &str = "fixed";
152const ANGLE_FN: &str = "angle";
153const HORIZONTAL_DISTANCE_FN: &str = "horizontalDistance";
154const VERTICAL_DISTANCE_FN: &str = "verticalDistance";
155const EQUAL_LENGTH_FN: &str = "equalLength";
156const EQUAL_RADIUS_FN: &str = "equalRadius";
157const HORIZONTAL_FN: &str = "horizontal";
158const MIDPOINT_FN: &str = "midpoint";
159const MIDPOINT_POINT_PARAM: &str = "point";
160const RADIUS_FN: &str = "radius";
161const SYMMETRIC_FN: &str = "symmetric";
162const SYMMETRIC_AXIS_PARAM: &str = "axis";
163const TANGENT_FN: &str = "tangent";
164const VERTICAL_FN: &str = "vertical";
165
166const LINE_PROPERTY_START: &str = "start";
167const LINE_PROPERTY_END: &str = "end";
168
169const ARC_PROPERTY_START: &str = "start";
170const ARC_PROPERTY_END: &str = "end";
171const ARC_PROPERTY_CENTER: &str = "center";
172const CIRCLE_PROPERTY_START: &str = "start";
173const CIRCLE_PROPERTY_CENTER: &str = "center";
174const CONTROL_POINT_SPLINE_PROPERTY_CONTROLS: &str = "controls";
175const CONTROL_POINT_SPLINE_PROPERTY_EDGES: &str = "edges";
176
177const CONSTRUCTION_PARAM: &str = "construction";
178
179#[derive(Debug, Clone, Copy)]
180enum EditDeleteKind {
181 Edit,
182 DeleteNonSketch,
183}
184
185struct ExecuteAfterEditOptions {
187 segment_ids_edited: AhashIndexSet<ObjectId>,
188 edit_kind: EditDeleteKind,
189 commit_solved_initial_guesses: bool,
190}
191
192impl EditDeleteKind {
193 fn is_delete(&self) -> bool {
195 match self {
196 EditDeleteKind::Edit => false,
197 EditDeleteKind::DeleteNonSketch => true,
198 }
199 }
200
201 fn to_change_kind(self) -> ChangeKind {
202 match self {
203 EditDeleteKind::Edit => ChangeKind::Edit,
204 EditDeleteKind::DeleteNonSketch => ChangeKind::Delete,
205 }
206 }
207}
208
209#[derive(Debug, Clone, Copy)]
210enum ChangeKind {
211 Add,
212 Edit,
213 Delete,
214 None,
215}
216
217#[derive(Debug, Clone, Serialize, ts_rs::TS)]
218#[ts(export, export_to = "FrontendApi.ts")]
219#[serde(tag = "type")]
220pub enum SetProgramOutcome {
221 #[serde(rename_all = "camelCase")]
222 Success {
223 scene_graph: Box<SceneGraph>,
224 exec_outcome: Box<ExecOutcome>,
225 checkpoint_id: Option<SketchCheckpointId>,
226 },
227 #[serde(rename_all = "camelCase")]
228 ExecFailure { error: Box<KclErrorWithOutputs> },
229}
230
231pub struct EditSegmentsOptions {
233 pub anchor_segment_ids: Option<Vec<ObjectId>>,
239 pub drag_anchors: Vec<SegmentDragAnchor>,
242 pub constraint_label_edits: Vec<ConstraintLabelPositionEdit>,
245 pub commit_solved_initial_guesses: bool,
247}
248
249pub struct EditDistanceConstraintLabelPositionOptions {
251 pub anchor_segment_ids: Vec<ObjectId>,
253 pub commit_solved_initial_guesses: bool,
255}
256
257#[derive(Debug, Clone)]
258struct SolidAstReference {
259 variable_name: String,
260 output_index: Option<usize>,
261}
262
263#[derive(Debug, Clone)]
264pub struct FrontendState {
265 program: Program,
266 scene_graph: SceneGraph,
267 solid_references: HashMap<Uuid, SolidAstReference>,
269 point_freedom_cache: HashMap<ObjectId, Freedom>,
272 next_drag_anchor_segment_ids: Option<AhashIndexSet<ObjectId>>,
275 next_segment_drag_anchors: Option<Vec<SegmentDragAnchor>>,
278 next_constraint_label_edits: Option<Vec<ConstraintLabelPositionEdit>>,
280 next_edit_commits_solver_solutions: Option<bool>,
284 sketch_checkpoints: VecDeque<SketchCheckpoint>,
285 sketch_checkpoint_id_gen: IncIdGenerator<u64>,
286}
287
288impl Default for FrontendState {
289 fn default() -> Self {
290 Self::new()
291 }
292}
293
294impl FrontendState {
295 pub fn new() -> Self {
296 Self {
297 program: Program::empty(),
298 scene_graph: SceneGraph {
299 project: ProjectId(0),
300 file: FileId(0),
301 version: Version(0),
302 objects: Default::default(),
303 settings: Default::default(),
304 sketch_mode: Default::default(),
305 },
306 solid_references: HashMap::new(),
307 point_freedom_cache: HashMap::new(),
308 next_drag_anchor_segment_ids: None,
309 next_segment_drag_anchors: None,
310 next_constraint_label_edits: None,
311 next_edit_commits_solver_solutions: None,
312 sketch_checkpoints: VecDeque::new(),
313 sketch_checkpoint_id_gen: IncIdGenerator::new(1),
314 }
315 }
316
317 pub fn scene_graph(&self) -> &SceneGraph {
319 &self.scene_graph
320 }
321
322 pub fn default_length_unit(&self) -> UnitLength {
323 self.program
324 .meta_settings()
325 .ok()
326 .flatten()
327 .map(|settings| settings.default_length_units)
328 .unwrap_or(UnitLength::Millimeters)
329 }
330
331 pub async fn create_sketch_checkpoint(&mut self, exec_outcome: ExecOutcome) -> api::Result<SketchCheckpointId> {
332 let checkpoint_id = SketchCheckpointId::new(self.sketch_checkpoint_id_gen.next_id());
333
334 let checkpoint = SketchCheckpoint {
335 id: checkpoint_id,
336 source: SourceDelta {
337 text: source_from_ast(&self.program.ast),
338 },
339 program: self.program.clone(),
340 scene_graph: self.scene_graph.clone(),
341 exec_outcome,
342 point_freedom_cache: self.point_freedom_cache.clone(),
343 mock_memory: read_old_memory().await,
344 };
345
346 self.sketch_checkpoints.push_back(checkpoint);
347 while self.sketch_checkpoints.len() > MAX_SKETCH_CHECKPOINTS {
348 self.sketch_checkpoints.pop_front();
349 }
350
351 Ok(checkpoint_id)
352 }
353
354 pub async fn edit_segments_with_options(
362 &mut self,
363 ctx: &ExecutorContext,
364 version: Version,
365 sketch: ObjectId,
366 segments: Vec<ExistingSegmentCtor>,
367 options: EditSegmentsOptions,
368 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
369 let previous_anchor_ids = options.anchor_segment_ids.map(|anchor_ids| {
370 self.next_drag_anchor_segment_ids
371 .replace(anchor_ids.into_iter().collect())
372 });
373 let previous_drag_anchors = self.next_segment_drag_anchors.replace(options.drag_anchors);
374 let previous_constraint_label_edits = self.next_constraint_label_edits.replace(options.constraint_label_edits);
375 let previous_commit_mode = self
376 .next_edit_commits_solver_solutions
377 .replace(options.commit_solved_initial_guesses);
378 let result = SketchApi::edit_segments(self, ctx, version, sketch, segments).await;
379 if let Some(previous_anchor_ids) = previous_anchor_ids {
380 self.next_drag_anchor_segment_ids = previous_anchor_ids;
381 }
382 self.next_segment_drag_anchors = previous_drag_anchors;
383 self.next_constraint_label_edits = previous_constraint_label_edits;
384 self.next_edit_commits_solver_solutions = previous_commit_mode;
385 result
386 }
387
388 pub async fn edit_distance_constraint_label_position_with_options(
394 &mut self,
395 ctx: &ExecutorContext,
396 version: Version,
397 sketch: ObjectId,
398 constraint_id: ObjectId,
399 label_position: Point2d<Number>,
400 options: EditDistanceConstraintLabelPositionOptions,
401 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
402 let previous_commit_mode = self
403 .next_edit_commits_solver_solutions
404 .replace(options.commit_solved_initial_guesses);
405 let result = SketchApi::edit_distance_constraint_label_position(
406 self,
407 ctx,
408 version,
409 sketch,
410 constraint_id,
411 label_position,
412 options.anchor_segment_ids,
413 )
414 .await;
415 self.next_edit_commits_solver_solutions = previous_commit_mode;
416 result
417 }
418
419 pub async fn restore_sketch_checkpoint(
420 &mut self,
421 checkpoint_id: SketchCheckpointId,
422 ) -> api::Result<RestoreSketchCheckpointOutcome> {
423 let checkpoint = self
424 .sketch_checkpoints
425 .iter()
426 .find(|checkpoint| checkpoint.id == checkpoint_id)
427 .cloned()
428 .ok_or_else(|| Error {
429 msg: format!("Sketch checkpoint not found: {checkpoint_id:?}"),
430 })?;
431
432 self.program = checkpoint.program;
433 self.scene_graph = checkpoint.scene_graph.clone();
434 self.solid_references = solid_references_from_variables(&self.program.ast, &checkpoint.exec_outcome.variables);
435 self.point_freedom_cache = checkpoint.point_freedom_cache;
436 self.next_drag_anchor_segment_ids = None;
437 self.next_segment_drag_anchors = None;
438 self.next_constraint_label_edits = None;
439 self.next_edit_commits_solver_solutions = None;
440
441 if let Some(mock_memory) = checkpoint.mock_memory {
442 write_old_memory(mock_memory).await;
443 } else {
444 clear_mem_cache().await;
445 }
446
447 Ok(RestoreSketchCheckpointOutcome {
448 source_delta: checkpoint.source,
449 scene_graph_delta: SceneGraphDelta {
450 new_graph: self.scene_graph_for_ui(),
451 new_objects: Vec::new(),
452 invalidates_ids: true,
453 exec_outcome: checkpoint.exec_outcome,
454 },
455 })
456 }
457
458 pub fn clear_sketch_checkpoints(&mut self) {
459 self.sketch_checkpoints.clear();
460 }
461 fn scene_graph_for_ui(&self) -> SceneGraph {
462 let has_control_point_splines = self.scene_graph.objects.iter().any(|object| {
463 matches!(
464 object.kind,
465 ObjectKind::Segment {
466 segment: Segment::ControlPointSpline(_)
467 }
468 )
469 });
470
471 if !has_control_point_splines {
472 return self.scene_graph.clone();
473 }
474
475 let hidden_constraint_ids = self
476 .scene_graph
477 .objects
478 .iter()
479 .filter_map(|object| match &object.kind {
480 ObjectKind::Constraint {
481 constraint: Constraint::Coincident(coincident),
482 } if coincident_is_internal_to_same_control_point_spline(coincident, &self.scene_graph) => {
483 Some(object.id)
484 }
485 _ => None,
486 })
487 .collect::<HashSet<_>>();
488
489 if hidden_constraint_ids.is_empty() {
490 return self.scene_graph.clone();
491 }
492
493 let mut scene_graph = self.scene_graph.clone();
494 for object in &mut scene_graph.objects {
495 match &mut object.kind {
496 ObjectKind::Constraint { .. } if hidden_constraint_ids.contains(&object.id) => {
497 object.kind = ObjectKind::Nil;
498 }
499 ObjectKind::Sketch(sketch) => {
500 sketch
501 .constraints
502 .retain(|constraint_id| !hidden_constraint_ids.contains(constraint_id));
503 }
504 _ => {}
505 }
506 }
507
508 scene_graph
509 }
510}
511
512fn coincident_is_internal_to_same_control_point_spline(coincident: &Coincident, scene_graph: &SceneGraph) -> bool {
513 let mut first_owner_id = None;
514 for segment_id in coincident.segment_ids() {
515 let Some(owner_id) = owning_control_point_spline_id(segment_id, scene_graph) else {
516 return false;
517 };
518
519 match first_owner_id {
520 Some(first_owner_id) if first_owner_id != owner_id => return false,
521 Some(_) => {}
522 None => first_owner_id = Some(owner_id),
523 }
524 }
525
526 first_owner_id.is_some()
527}
528
529fn owning_control_point_spline_id(segment_id: ObjectId, scene_graph: &SceneGraph) -> Option<ObjectId> {
530 let object = scene_graph.objects.get(segment_id.0)?;
531 let ObjectKind::Segment { segment } = &object.kind else {
532 return None;
533 };
534
535 match segment {
536 Segment::ControlPointSpline(_) => Some(segment_id),
537 Segment::Point(point) => point
538 .owner
539 .filter(|owner_id| matches_control_point_spline_owner(*owner_id, scene_graph)),
540 Segment::Line(line) => line
541 .owner
542 .filter(|owner_id| matches_control_point_spline_owner(*owner_id, scene_graph)),
543 _ => None,
544 }
545}
546
547fn matches_control_point_spline_owner(owner_id: ObjectId, scene_graph: &SceneGraph) -> bool {
548 matches!(
549 scene_graph.objects.get(owner_id.0).map(|object| &object.kind),
550 Some(ObjectKind::Segment {
551 segment: Segment::ControlPointSpline(_)
552 })
553 )
554}
555
556fn ensure_control_point_spline_experimental_features(program: &Program) -> Result<Program, KclError> {
557 let experimental_features_allowed = program
558 .meta_settings()
559 .ok()
560 .flatten()
561 .map(|settings| settings.experimental_features == WarningLevel::Allow)
562 .unwrap_or(false);
563 if experimental_features_allowed {
564 return Ok(program.clone());
565 }
566
567 program.change_experimental_features(Some(WarningLevel::Allow))
568}
569
570impl SketchApi for FrontendState {
571 async fn execute_mock(
572 &mut self,
573 ctx: &ExecutorContext,
574 _version: Version,
575 sketch: ObjectId,
576 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
577 let sketch_block_ref =
578 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
579
580 let mut truncated_program = self.program.clone();
581 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::None)
582 .map_err(KclErrorWithOutputs::no_outputs)?;
583
584 let outcome = ctx
586 .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch))
587 .await?;
588 let new_source = source_from_ast(&self.program.ast);
589 let src_delta = SourceDelta { text: new_source };
590 let outcome = self.update_state_after_exec(outcome, true);
592 let scene_graph_delta = SceneGraphDelta {
593 new_graph: self.scene_graph.clone(),
594 new_objects: Default::default(),
595 invalidates_ids: false,
596 exec_outcome: outcome,
597 };
598 Ok((src_delta, scene_graph_delta))
599 }
600
601 async fn new_sketch(
602 &mut self,
603 ctx: &ExecutorContext,
604 _project: ProjectId,
605 _file: FileId,
606 _version: Version,
607 args: SketchCtor,
608 ) -> ExecResult<(SourceDelta, SceneGraphDelta, ObjectId)> {
609 let mut new_ast = self.program.ast.clone();
612 let mut plane_ast = sketch_on_ast_expr(&mut new_ast, &self.scene_graph, &self.solid_references, &args.on)
614 .map_err(KclErrorWithOutputs::no_outputs)?;
615 let mut defined_names = find_defined_names(&new_ast);
616 let is_face_of_expr = matches!(
617 &plane_ast,
618 ast::Expr::CallExpressionKw(call) if call.callee.name.name == "faceOf"
619 );
620 if is_face_of_expr {
621 let face_name = next_free_name_with_padding("face", &defined_names)
622 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
623 let face_decl = ast::VariableDeclaration::new(
624 ast::VariableDeclarator::new(&face_name, plane_ast),
625 ast::ItemVisibility::Default,
626 ast::VariableKind::Const,
627 );
628 new_ast
629 .body
630 .push(ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(
631 face_decl,
632 ))));
633 defined_names.insert(face_name.clone());
634 plane_ast = ast::Expr::Name(Box::new(ast::Name::new(&face_name)));
635 }
636 let sketch_ast = ast::SketchBlock {
637 arguments: vec![ast::LabeledArg {
638 label: Some(ast::Identifier::new(SKETCH_BLOCK_PARAM_ON)),
639 arg: plane_ast,
640 }],
641 body: Default::default(),
642 is_being_edited: false,
643 non_code_meta: Default::default(),
644 digest: None,
645 };
646 let sketch_name = next_free_name_with_padding("sketch", &defined_names)
649 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
650 let sketch_decl = ast::VariableDeclaration::new(
651 ast::VariableDeclarator::new(
652 &sketch_name,
653 ast::Expr::SketchBlock(Box::new(ast::Node::no_src(sketch_ast))),
654 ),
655 ast::ItemVisibility::Default,
656 ast::VariableKind::Const,
657 );
658 new_ast
659 .body
660 .push(ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(
661 sketch_decl,
662 ))));
663 let new_source = source_from_ast(&new_ast);
665 let new_program = parse_frontend_mutation_source(
667 &new_source,
668 "Error parsing KCL source after adding sketch",
669 "No AST produced after adding sketch",
670 )?;
671
672 self.program = new_program.clone();
674
675 let outcome = ctx.run_with_caching(new_program.clone()).await?;
678 let freedom_analysis_ran = true;
679
680 let outcome = self.update_state_after_exec(outcome, freedom_analysis_ran);
681
682 let Some(sketch_id) = self
683 .scene_graph
684 .objects
685 .iter()
686 .filter_map(|object| match object.kind {
687 ObjectKind::Sketch(_) => Some(object.id),
688 _ => None,
689 })
690 .max_by_key(|id| id.0)
691 else {
692 return Err(KclErrorWithOutputs::from_error_outcome(
693 KclError::refactor("No objects in scene graph after adding sketch".to_owned()),
694 outcome,
695 ));
696 };
697 self.scene_graph.sketch_mode = Some(sketch_id);
699
700 let src_delta = SourceDelta { text: new_source };
701 let scene_graph_delta = SceneGraphDelta {
702 new_graph: self.scene_graph_for_ui(),
703 invalidates_ids: false,
704 new_objects: vec![sketch_id],
705 exec_outcome: outcome,
706 };
707 Ok((src_delta, scene_graph_delta, sketch_id))
708 }
709
710 async fn edit_sketch(
711 &mut self,
712 ctx: &ExecutorContext,
713 _project: ProjectId,
714 _file: FileId,
715 _version: Version,
716 sketch: ObjectId,
717 ) -> ExecResult<SceneGraphDelta> {
718 let sketch_object = self.scene_graph.objects.get(sketch.0).ok_or_else(|| {
722 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
723 })?;
724 let ObjectKind::Sketch(_) = &sketch_object.kind else {
725 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
726 "Object is not a sketch, it is {}",
727 sketch_object.kind.human_friendly_kind_with_article()
728 ))));
729 };
730 let sketch_block_ref = expect_single_node_ref(sketch_object).map_err(KclErrorWithOutputs::no_outputs)?;
731
732 self.scene_graph.sketch_mode = Some(sketch);
734
735 let mut truncated_program = self.program.clone();
737 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::None)
738 .map_err(KclErrorWithOutputs::no_outputs)?;
739
740 let outcome = ctx
743 .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch))
744 .await?;
745
746 let outcome = self.update_state_after_exec(outcome, true);
748 let scene_graph_delta = SceneGraphDelta {
749 new_graph: self.scene_graph_for_ui(),
750 invalidates_ids: false,
751 new_objects: Vec::new(),
752 exec_outcome: outcome,
753 };
754 Ok(scene_graph_delta)
755 }
756
757 async fn exit_sketch(
758 &mut self,
759 ctx: &ExecutorContext,
760 _version: Version,
761 sketch: ObjectId,
762 ) -> ExecResult<SceneGraph> {
763 #[cfg(not(target_arch = "wasm32"))]
765 let _ = sketch;
766 #[cfg(target_arch = "wasm32")]
767 if self.scene_graph.sketch_mode != Some(sketch) {
768 web_sys::console::warn_1(
769 &format!(
770 "WARNING: exit_sketch: current state's sketch mode ID doesn't match the given sketch ID; state={:#?}, given={sketch:?}",
771 self.scene_graph.sketch_mode
772 )
773 .into(),
774 );
775 }
776 self.scene_graph.sketch_mode = None;
777
778 let outcome = ctx.run_with_caching(self.program.clone()).await?;
780
781 self.update_state_after_exec(outcome, false);
783
784 Ok(self.scene_graph_for_ui())
785 }
786
787 async fn delete_sketch(
788 &mut self,
789 ctx: &ExecutorContext,
790 _version: Version,
791 sketch: ObjectId,
792 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
793 let mut new_ast = self.program.ast.clone();
796
797 let sketch_id = sketch;
799 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
800 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
801 })?;
802 let ObjectKind::Sketch(_) = &sketch_object.kind else {
803 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
804 "Object is not a sketch, it is {}",
805 sketch_object.kind.human_friendly_kind_with_article(),
806 ))));
807 };
808
809 self.mutate_ast(&mut new_ast, sketch_id, AstMutateCommand::DeleteNode)
811 .map_err(KclErrorWithOutputs::no_outputs)?;
812
813 self.execute_after_delete_sketch(ctx, &mut new_ast).await
814 }
815
816 async fn add_segment(
817 &mut self,
818 ctx: &ExecutorContext,
819 _version: Version,
820 sketch: ObjectId,
821 segment: SegmentCtor,
822 _label: Option<String>,
823 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
824 match segment {
826 SegmentCtor::Point(ctor) => self.add_point(ctx, sketch, ctor).await,
827 SegmentCtor::Line(ctor) => self.add_line(ctx, sketch, ctor).await,
828 SegmentCtor::Arc(ctor) => self.add_arc(ctx, sketch, ctor).await,
829 SegmentCtor::Circle(ctor) => self.add_circle(ctx, sketch, ctor).await,
830 SegmentCtor::ControlPointSpline(ctor) => self.add_control_point_spline(ctx, sketch, ctor).await,
831 }
832 }
833
834 async fn edit_segments(
835 &mut self,
836 ctx: &ExecutorContext,
837 _version: Version,
838 sketch: ObjectId,
839 segments: Vec<ExistingSegmentCtor>,
840 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
841 let sketch_block_ref =
843 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
844
845 let mut new_ast = self.program.ast.clone();
846 let mut edited_segment_ids = AhashIndexSet::with_capacity_and_hasher(segments.len(), Default::default());
847 let mut invalidates_ids = false;
848
849 for segment in &segments {
852 edited_segment_ids.insert(segment.id);
853 if let SegmentCtor::ControlPointSpline(new_ctor) = &segment.ctor
854 && let Some(existing_object) = self.scene_graph.objects.get(segment.id.0)
855 && let ObjectKind::Segment {
856 segment: Segment::ControlPointSpline(existing_spline),
857 } = &existing_object.kind
858 && existing_spline.controls.len() != new_ctor.points.len()
859 {
860 invalidates_ids = true;
861 }
862 }
863 let drag_anchor_segment_ids = self
864 .next_drag_anchor_segment_ids
865 .take()
866 .unwrap_or_else(|| edited_segment_ids.clone());
867 let constraint_label_edits = self.next_constraint_label_edits.take().unwrap_or_default();
868 let commit_solved_initial_guesses = self.next_edit_commits_solver_solutions.take().unwrap_or(true);
869
870 let mut final_edits: IndexMap<ObjectId, SegmentCtor> = IndexMap::new();
885
886 for segment in segments {
887 let segment_id = segment.id;
888 match segment.ctor {
889 SegmentCtor::Point(ctor) => {
890 if let Some(segment_object) = self.scene_graph.objects.get(segment_id.0)
892 && let ObjectKind::Segment { segment } = &segment_object.kind
893 && let Segment::Point(point) = segment
894 && let Some(owner_id) = point.owner
895 && let Some(owner_object) = self.scene_graph.objects.get(owner_id.0)
896 && let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind
897 {
898 match owner_segment {
899 Segment::Line(line) if line.start == segment_id || line.end == segment_id => {
900 if let Some(existing) = final_edits.get_mut(&owner_id) {
901 let SegmentCtor::Line(line_ctor) = existing else {
902 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
903 "Internal: Expected line ctor for owner, but found {}",
904 existing.human_friendly_kind_with_article()
905 ))));
906 };
907 if line.start == segment_id {
909 line_ctor.start = ctor.position;
910 } else {
911 line_ctor.end = ctor.position;
912 }
913 } else if let SegmentCtor::Line(line_ctor) = &line.ctor {
914 let mut line_ctor = line_ctor.clone();
916 if line.start == segment_id {
917 line_ctor.start = ctor.position;
918 } else {
919 line_ctor.end = ctor.position;
920 }
921 final_edits.insert(owner_id, SegmentCtor::Line(line_ctor));
922 } else {
923 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
925 "Internal: Line does not have line ctor, but found {}",
926 line.ctor.human_friendly_kind_with_article()
927 ))));
928 }
929 continue;
930 }
931 Segment::Arc(arc)
932 if arc.start == segment_id || arc.end == segment_id || arc.center == segment_id =>
933 {
934 if let Some(existing) = final_edits.get_mut(&owner_id) {
935 let SegmentCtor::Arc(arc_ctor) = existing else {
936 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
937 "Internal: Expected arc ctor for owner, but found {}",
938 existing.human_friendly_kind_with_article()
939 ))));
940 };
941 if arc.start == segment_id {
942 arc_ctor.start = ctor.position;
943 } else if arc.end == segment_id {
944 arc_ctor.end = ctor.position;
945 } else {
946 arc_ctor.center = ctor.position;
947 }
948 } else if let SegmentCtor::Arc(arc_ctor) = &arc.ctor {
949 let mut arc_ctor = arc_ctor.clone();
950 if arc.start == segment_id {
951 arc_ctor.start = ctor.position;
952 } else if arc.end == segment_id {
953 arc_ctor.end = ctor.position;
954 } else {
955 arc_ctor.center = ctor.position;
956 }
957 final_edits.insert(owner_id, SegmentCtor::Arc(arc_ctor));
958 } else {
959 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
960 "Internal: Arc does not have arc ctor, but found {}",
961 arc.ctor.human_friendly_kind_with_article()
962 ))));
963 }
964 continue;
965 }
966 Segment::Circle(circle) if circle.start == segment_id || circle.center == segment_id => {
967 if let Some(existing) = final_edits.get_mut(&owner_id) {
968 let SegmentCtor::Circle(circle_ctor) = existing else {
969 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
970 "Internal: Expected circle ctor for owner, but found {}",
971 existing.human_friendly_kind_with_article()
972 ))));
973 };
974 if circle.start == segment_id {
975 circle_ctor.start = ctor.position;
976 } else {
977 circle_ctor.center = ctor.position;
978 }
979 } else if let SegmentCtor::Circle(circle_ctor) = &circle.ctor {
980 let mut circle_ctor = circle_ctor.clone();
981 if circle.start == segment_id {
982 circle_ctor.start = ctor.position;
983 } else {
984 circle_ctor.center = ctor.position;
985 }
986 final_edits.insert(owner_id, SegmentCtor::Circle(circle_ctor));
987 } else {
988 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
989 "Internal: Circle does not have circle ctor, but found {}",
990 circle.ctor.human_friendly_kind_with_article()
991 ))));
992 }
993 continue;
994 }
995 Segment::ControlPointSpline(spline) if spline.controls.contains(&segment_id) => {
996 let Some(control_index) =
997 spline.controls.iter().position(|control_id| *control_id == segment_id)
998 else {
999 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1000 "Internal: Point is not part of owner's controlPointSpline segment: point={segment_id:?}, spline={owner_id:?}"
1001 ))));
1002 };
1003 if let Some(existing) = final_edits.get_mut(&owner_id) {
1004 let SegmentCtor::ControlPointSpline(spline_ctor) = existing else {
1005 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1006 "Internal: Expected controlPointSpline ctor for owner, but found {}",
1007 existing.human_friendly_kind_with_article()
1008 ))));
1009 };
1010 spline_ctor.points[control_index] = ctor.position;
1011 } else if let SegmentCtor::ControlPointSpline(spline_ctor) = &spline.ctor {
1012 let mut spline_ctor = spline_ctor.clone();
1013 spline_ctor.points[control_index] = ctor.position;
1014 final_edits.insert(owner_id, SegmentCtor::ControlPointSpline(spline_ctor));
1015 } else {
1016 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1017 "Internal: Control point spline does not have controlPointSpline ctor, but found {}",
1018 spline.ctor.human_friendly_kind_with_article()
1019 ))));
1020 }
1021 continue;
1022 }
1023 _ => {}
1024 }
1025 }
1026
1027 final_edits.insert(segment_id, SegmentCtor::Point(ctor));
1029 }
1030 SegmentCtor::Line(ctor) => {
1031 final_edits.insert(segment_id, SegmentCtor::Line(ctor));
1032 }
1033 SegmentCtor::Arc(ctor) => {
1034 final_edits.insert(segment_id, SegmentCtor::Arc(ctor));
1035 }
1036 SegmentCtor::Circle(ctor) => {
1037 final_edits.insert(segment_id, SegmentCtor::Circle(ctor));
1038 }
1039 SegmentCtor::ControlPointSpline(ctor) => {
1040 final_edits.insert(segment_id, SegmentCtor::ControlPointSpline(ctor));
1041 }
1042 }
1043 }
1044
1045 for (segment_id, ctor) in final_edits {
1046 match ctor {
1047 SegmentCtor::Point(ctor) => self
1048 .edit_point(&mut new_ast, sketch, segment_id, ctor)
1049 .map_err(KclErrorWithOutputs::no_outputs)?,
1050 SegmentCtor::Line(ctor) => self
1051 .edit_line(&mut new_ast, sketch, segment_id, ctor)
1052 .map_err(KclErrorWithOutputs::no_outputs)?,
1053 SegmentCtor::Arc(ctor) => self
1054 .edit_arc(&mut new_ast, sketch, segment_id, ctor)
1055 .map_err(KclErrorWithOutputs::no_outputs)?,
1056 SegmentCtor::Circle(ctor) => self
1057 .edit_circle(&mut new_ast, sketch, segment_id, ctor)
1058 .map_err(KclErrorWithOutputs::no_outputs)?,
1059 SegmentCtor::ControlPointSpline(ctor) => self
1060 .edit_control_point_spline(&mut new_ast, sketch, segment_id, ctor)
1061 .map_err(KclErrorWithOutputs::no_outputs)?,
1062 }
1063 }
1064 for edit in constraint_label_edits {
1065 self.mutate_constraint_label_position(&mut new_ast, edit.constraint_id, edit.label_position)
1066 .map_err(KclErrorWithOutputs::no_outputs)?;
1067 }
1068 let (source_delta, mut scene_graph_delta) = self
1069 .execute_after_edit(
1070 ctx,
1071 sketch,
1072 sketch_block_ref,
1073 &mut new_ast,
1074 ExecuteAfterEditOptions {
1075 segment_ids_edited: drag_anchor_segment_ids,
1076 edit_kind: EditDeleteKind::Edit,
1077 commit_solved_initial_guesses,
1078 },
1079 )
1080 .await?;
1081 if invalidates_ids {
1082 scene_graph_delta.invalidates_ids = true;
1083 }
1084 Ok((source_delta, scene_graph_delta))
1085 }
1086
1087 async fn delete_objects(
1088 &mut self,
1089 ctx: &ExecutorContext,
1090 _version: Version,
1091 sketch: ObjectId,
1092 constraint_ids: Vec<ObjectId>,
1093 segment_ids: Vec<ObjectId>,
1094 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1095 let sketch_block_ref =
1097 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1098
1099 let mut constraint_ids_set = constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
1101 let segment_ids_set = segment_ids.into_iter().collect::<AhashIndexSet<_>>();
1102
1103 let mut resolved_segment_ids_to_delete = AhashIndexSet::default();
1106
1107 for segment_id in segment_ids_set.iter().copied() {
1108 let owner_id = self.scene_graph.objects.get(segment_id.0).and_then(|segment_object| {
1109 let ObjectKind::Segment { segment } = &segment_object.kind else {
1110 return None;
1111 };
1112 match segment {
1113 Segment::Point(point) => point.owner,
1114 Segment::Line(line) => line.owner,
1115 _ => None,
1116 }
1117 });
1118
1119 if let Some(owner_id) = owner_id
1120 && let Some(owner_object) = self.scene_graph.objects.get(owner_id.0)
1121 && let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind
1122 && matches!(
1123 owner_segment,
1124 Segment::Line(_) | Segment::Arc(_) | Segment::Circle(_) | Segment::ControlPointSpline(_)
1125 )
1126 {
1127 resolved_segment_ids_to_delete.insert(owner_id);
1129 } else {
1130 resolved_segment_ids_to_delete.insert(segment_id);
1132 }
1133 }
1134 let referenced_constraint_ids = self
1135 .find_referenced_constraints(sketch, &resolved_segment_ids_to_delete)
1136 .map_err(KclErrorWithOutputs::no_outputs)?;
1137
1138 let mut new_ast = self.program.ast.clone();
1139
1140 for constraint_id in referenced_constraint_ids {
1141 if constraint_ids_set.contains(&constraint_id) {
1142 continue;
1143 }
1144
1145 let constraint_object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
1146 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Constraint not found: {constraint_id:?}")))
1147 })?;
1148 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
1149 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1150 "Object is not a constraint, it is {}",
1151 constraint_object.kind.human_friendly_kind_with_article()
1152 ))));
1153 };
1154
1155 match constraint {
1156 Constraint::Coincident(coincident) => {
1157 let remaining_segments =
1158 self.remaining_constraint_segments(&coincident.segments, &resolved_segment_ids_to_delete);
1159
1160 if remaining_segments.len() >= 2 {
1162 self.edit_coincident_constraint(&mut new_ast, constraint_id, remaining_segments)
1163 .map_err(KclErrorWithOutputs::no_outputs)?;
1164 } else {
1165 constraint_ids_set.insert(constraint_id);
1166 }
1167 }
1168 Constraint::EqualRadius(equal_radius) => {
1169 let remaining_input = equal_radius
1170 .input
1171 .iter()
1172 .copied()
1173 .filter(|segment_id| {
1174 !self.segment_will_be_deleted(*segment_id, &resolved_segment_ids_to_delete)
1175 })
1176 .collect::<Vec<_>>();
1177
1178 if remaining_input.len() >= 2 {
1179 self.edit_equal_radius_constraint(&mut new_ast, constraint_id, remaining_input)
1180 .map_err(KclErrorWithOutputs::no_outputs)?;
1181 } else {
1182 constraint_ids_set.insert(constraint_id);
1183 }
1184 }
1185 Constraint::LinesEqualLength(lines_equal_length) => {
1186 let remaining_lines = lines_equal_length
1187 .lines
1188 .iter()
1189 .copied()
1190 .filter(|line_id| !self.segment_will_be_deleted(*line_id, &resolved_segment_ids_to_delete))
1191 .collect::<Vec<_>>();
1192
1193 if remaining_lines.len() >= 2 {
1195 self.edit_equal_length_constraint(&mut new_ast, constraint_id, remaining_lines)
1196 .map_err(KclErrorWithOutputs::no_outputs)?;
1197 } else {
1198 constraint_ids_set.insert(constraint_id);
1199 }
1200 }
1201 Constraint::Parallel(parallel) => {
1202 let remaining_lines = parallel
1203 .lines
1204 .iter()
1205 .copied()
1206 .filter(|line_id| !self.segment_will_be_deleted(*line_id, &resolved_segment_ids_to_delete))
1207 .collect::<Vec<_>>();
1208
1209 if remaining_lines.len() >= 2 {
1210 self.edit_parallel_constraint(&mut new_ast, constraint_id, remaining_lines)
1211 .map_err(KclErrorWithOutputs::no_outputs)?;
1212 } else {
1213 constraint_ids_set.insert(constraint_id);
1214 }
1215 }
1216 Constraint::Horizontal(Horizontal::Points { points }) => {
1217 let remaining_points = self.remaining_constraint_segments(points, &resolved_segment_ids_to_delete);
1218
1219 if remaining_points.len() >= 2 {
1220 self.edit_horizontal_points_constraint(&mut new_ast, constraint_id, remaining_points)
1221 .map_err(KclErrorWithOutputs::no_outputs)?;
1222 } else {
1223 constraint_ids_set.insert(constraint_id);
1224 }
1225 }
1226 Constraint::Vertical(Vertical::Points { points }) => {
1227 let remaining_points = self.remaining_constraint_segments(points, &resolved_segment_ids_to_delete);
1228
1229 if remaining_points.len() >= 2 {
1230 self.edit_vertical_points_constraint(&mut new_ast, constraint_id, remaining_points)
1231 .map_err(KclErrorWithOutputs::no_outputs)?;
1232 } else {
1233 constraint_ids_set.insert(constraint_id);
1234 }
1235 }
1236 Constraint::Fixed(fixed) => {
1237 if fixed.points.iter().any(|fixed_point| {
1238 self.segment_will_be_deleted(fixed_point.point, &resolved_segment_ids_to_delete)
1239 }) {
1240 constraint_ids_set.insert(constraint_id);
1241 }
1242 }
1243 _ => {
1244 constraint_ids_set.insert(constraint_id);
1246 }
1247 }
1248 }
1249
1250 for constraint_id in constraint_ids_set {
1251 self.delete_constraint(&mut new_ast, sketch, constraint_id)
1252 .map_err(KclErrorWithOutputs::no_outputs)?;
1253 }
1254 for segment_id in resolved_segment_ids_to_delete {
1255 self.delete_segment(&mut new_ast, sketch, segment_id)
1256 .map_err(KclErrorWithOutputs::no_outputs)?;
1257 }
1258
1259 self.execute_after_edit(
1260 ctx,
1261 sketch,
1262 sketch_block_ref,
1263 &mut new_ast,
1264 ExecuteAfterEditOptions {
1265 segment_ids_edited: Default::default(),
1266 edit_kind: EditDeleteKind::DeleteNonSketch,
1267 commit_solved_initial_guesses: true,
1268 },
1269 )
1270 .await
1271 }
1272
1273 async fn add_constraint(
1274 &mut self,
1275 ctx: &ExecutorContext,
1276 _version: Version,
1277 sketch: ObjectId,
1278 constraint: Constraint,
1279 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1280 let original_program = self.program.clone();
1284 let original_scene_graph = self.scene_graph.clone();
1285
1286 let mut new_ast = self.program.ast.clone();
1287 let sketch_block_ref = match constraint {
1288 Constraint::Coincident(coincident) => self
1289 .add_coincident(sketch, coincident, &mut new_ast)
1290 .await
1291 .map_err(KclErrorWithOutputs::no_outputs)?,
1292 Constraint::Distance(distance) => self
1293 .add_distance(sketch, distance, &mut new_ast)
1294 .await
1295 .map_err(KclErrorWithOutputs::no_outputs)?,
1296 Constraint::EqualRadius(equal_radius) => self
1297 .add_equal_radius(sketch, equal_radius, &mut new_ast)
1298 .await
1299 .map_err(KclErrorWithOutputs::no_outputs)?,
1300 Constraint::Fixed(fixed) => self
1301 .add_fixed_constraints(sketch, fixed.points, &mut new_ast)
1302 .await
1303 .map_err(KclErrorWithOutputs::no_outputs)?,
1304 Constraint::HorizontalDistance(distance) => self
1305 .add_horizontal_distance(sketch, distance, &mut new_ast)
1306 .await
1307 .map_err(KclErrorWithOutputs::no_outputs)?,
1308 Constraint::VerticalDistance(distance) => self
1309 .add_vertical_distance(sketch, distance, &mut new_ast)
1310 .await
1311 .map_err(KclErrorWithOutputs::no_outputs)?,
1312 Constraint::Horizontal(horizontal) => self
1313 .add_horizontal(sketch, horizontal, &mut new_ast)
1314 .await
1315 .map_err(KclErrorWithOutputs::no_outputs)?,
1316 Constraint::LinesEqualLength(lines_equal_length) => self
1317 .add_lines_equal_length(sketch, lines_equal_length, &mut new_ast)
1318 .await
1319 .map_err(KclErrorWithOutputs::no_outputs)?,
1320 Constraint::Midpoint(midpoint) => self
1321 .add_midpoint(sketch, midpoint, &mut new_ast)
1322 .await
1323 .map_err(KclErrorWithOutputs::no_outputs)?,
1324 Constraint::Parallel(parallel) => self
1325 .add_parallel(sketch, parallel, &mut new_ast)
1326 .await
1327 .map_err(KclErrorWithOutputs::no_outputs)?,
1328 Constraint::Perpendicular(perpendicular) => self
1329 .add_perpendicular(sketch, perpendicular, &mut new_ast)
1330 .await
1331 .map_err(KclErrorWithOutputs::no_outputs)?,
1332 Constraint::Radius(radius) => self
1333 .add_radius(sketch, radius, &mut new_ast)
1334 .await
1335 .map_err(KclErrorWithOutputs::no_outputs)?,
1336 Constraint::Diameter(diameter) => self
1337 .add_diameter(sketch, diameter, &mut new_ast)
1338 .await
1339 .map_err(KclErrorWithOutputs::no_outputs)?,
1340 Constraint::Symmetric(symmetric) => self
1341 .add_symmetric(sketch, symmetric, &mut new_ast)
1342 .await
1343 .map_err(KclErrorWithOutputs::no_outputs)?,
1344 Constraint::Vertical(vertical) => self
1345 .add_vertical(sketch, vertical, &mut new_ast)
1346 .await
1347 .map_err(KclErrorWithOutputs::no_outputs)?,
1348 Constraint::Angle(lines_at_angle) => self
1349 .add_angle(sketch, lines_at_angle, &mut new_ast)
1350 .await
1351 .map_err(KclErrorWithOutputs::no_outputs)?,
1352 Constraint::Tangent(tangent) => self
1353 .add_tangent(sketch, tangent, &mut new_ast)
1354 .await
1355 .map_err(KclErrorWithOutputs::no_outputs)?,
1356 };
1357
1358 let result = self
1359 .execute_after_add_constraint(ctx, sketch, sketch_block_ref, &mut new_ast)
1360 .await;
1361
1362 if result.is_err() {
1364 self.program = original_program;
1365 self.scene_graph = original_scene_graph;
1366 }
1367
1368 result
1369 }
1370
1371 async fn chain_segment(
1372 &mut self,
1373 ctx: &ExecutorContext,
1374 version: Version,
1375 sketch: ObjectId,
1376 previous_segment_end_point_id: ObjectId,
1377 segment: SegmentCtor,
1378 _label: Option<String>,
1379 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1380 let SegmentCtor::Line(line_ctor) = segment else {
1384 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1385 "chain_segment currently only supports Line segments, got {}",
1386 segment.human_friendly_kind_with_article(),
1387 ))));
1388 };
1389
1390 let (_first_src_delta, first_scene_delta) = self.add_line(ctx, sketch, line_ctor).await?;
1392
1393 let new_line_id = first_scene_delta
1396 .new_objects
1397 .iter()
1398 .find(|&obj_id| {
1399 let obj = self.scene_graph.objects.get(obj_id.0);
1400 if let Some(obj) = obj {
1401 matches!(
1402 &obj.kind,
1403 ObjectKind::Segment {
1404 segment: Segment::Line(_)
1405 }
1406 )
1407 } else {
1408 false
1409 }
1410 })
1411 .ok_or_else(|| {
1412 KclErrorWithOutputs::no_outputs(KclError::refactor(
1413 "Failed to find new line segment in scene graph".to_string(),
1414 ))
1415 })?;
1416
1417 let new_line_obj = self.scene_graph.objects.get(new_line_id.0).ok_or_else(|| {
1418 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1419 "New line object not found: {new_line_id:?}"
1420 )))
1421 })?;
1422
1423 let ObjectKind::Segment {
1424 segment: new_line_segment,
1425 } = &new_line_obj.kind
1426 else {
1427 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1428 "Object is not a segment: {new_line_obj:?}"
1429 ))));
1430 };
1431
1432 let Segment::Line(new_line) = new_line_segment else {
1433 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1434 "Segment is not a line: {new_line_segment:?}"
1435 ))));
1436 };
1437
1438 let new_line_start_point_id = new_line.start;
1439
1440 let coincident = Coincident {
1442 segments: vec![previous_segment_end_point_id.into(), new_line_start_point_id.into()],
1443 };
1444
1445 let (final_src_delta, final_scene_delta) = self
1446 .add_constraint(ctx, version, sketch, Constraint::Coincident(coincident))
1447 .await?;
1448
1449 let mut combined_new_objects = first_scene_delta.new_objects.clone();
1452 combined_new_objects.extend(final_scene_delta.new_objects);
1453
1454 let scene_graph_delta = SceneGraphDelta {
1455 new_graph: self.scene_graph_for_ui(),
1456 invalidates_ids: false,
1457 new_objects: combined_new_objects,
1458 exec_outcome: final_scene_delta.exec_outcome,
1459 };
1460
1461 Ok((final_src_delta, scene_graph_delta))
1462 }
1463
1464 async fn edit_constraint(
1465 &mut self,
1466 ctx: &ExecutorContext,
1467 _version: Version,
1468 sketch: ObjectId,
1469 constraint_id: ObjectId,
1470 value_expression: String,
1471 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1472 let sketch_block_ref =
1474 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1475
1476 let object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
1477 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Object not found: {constraint_id:?}")))
1478 })?;
1479 if !matches!(&object.kind, ObjectKind::Constraint { .. }) {
1480 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1481 "Object is not a constraint: {constraint_id:?}"
1482 ))));
1483 }
1484
1485 let mut new_ast = self.program.ast.clone();
1486
1487 let (parsed, errors) = Program::parse(&value_expression).map_err(|e| {
1489 KclErrorWithOutputs::no_outputs(KclError::refactor(format_kcl_error_message(
1490 "Invalid constraint value",
1491 &e,
1492 )))
1493 })?;
1494 if !errors.is_empty() {
1495 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1496 format_compilation_issues("Invalid constraint value", &errors),
1497 )));
1498 }
1499 let mut parsed = parsed.ok_or_else(|| {
1500 KclErrorWithOutputs::no_outputs(KclError::refactor("No AST produced from value expression".to_string()))
1501 })?;
1502 if parsed.ast.body.is_empty() {
1503 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1504 "Empty value expression".to_string(),
1505 )));
1506 }
1507 let first = parsed.ast.body.remove(0);
1508 let ast::BodyItem::ExpressionStatement(expr_stmt) = first else {
1509 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1510 "Value expression must be a simple expression".to_string(),
1511 )));
1512 };
1513
1514 let new_value: ast::BinaryPart = expr_stmt
1515 .inner
1516 .expression
1517 .try_into()
1518 .map_err(|e: String| KclErrorWithOutputs::no_outputs(KclError::refactor(e)))?;
1519
1520 self.mutate_ast(
1521 &mut new_ast,
1522 constraint_id,
1523 AstMutateCommand::EditConstraintValue { value: new_value },
1524 )
1525 .map_err(KclErrorWithOutputs::no_outputs)?;
1526
1527 self.execute_after_edit(
1528 ctx,
1529 sketch,
1530 sketch_block_ref,
1531 &mut new_ast,
1532 ExecuteAfterEditOptions {
1533 segment_ids_edited: Default::default(),
1534 edit_kind: EditDeleteKind::Edit,
1535 commit_solved_initial_guesses: true,
1536 },
1537 )
1538 .await
1539 }
1540
1541 async fn edit_distance_constraint_label_position(
1542 &mut self,
1543 ctx: &ExecutorContext,
1544 _version: Version,
1545 sketch: ObjectId,
1546 constraint_id: ObjectId,
1547 label_position: Point2d<Number>,
1548 anchor_segment_ids: Vec<ObjectId>,
1549 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1550 let sketch_block_ref =
1552 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1553
1554 let mut new_ast = self.program.ast.clone();
1555 self.mutate_constraint_label_position(&mut new_ast, constraint_id, label_position)
1556 .map_err(KclErrorWithOutputs::no_outputs)?;
1557 let commit_solved_initial_guesses = self.next_edit_commits_solver_solutions.take().unwrap_or(true);
1558
1559 self.execute_after_edit(
1560 ctx,
1561 sketch,
1562 sketch_block_ref,
1563 &mut new_ast,
1564 ExecuteAfterEditOptions {
1565 segment_ids_edited: anchor_segment_ids.into_iter().collect(),
1566 edit_kind: EditDeleteKind::Edit,
1567 commit_solved_initial_guesses,
1568 },
1569 )
1570 .await
1571 }
1572
1573 async fn batch_split_segment_operations(
1581 &mut self,
1582 ctx: &ExecutorContext,
1583 _version: Version,
1584 sketch: ObjectId,
1585 edit_segments: Vec<ExistingSegmentCtor>,
1586 add_constraints: Vec<Constraint>,
1587 delete_constraint_ids: Vec<ObjectId>,
1588 _new_segment_info: sketch::NewSegmentInfo,
1589 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1590 let sketch_block_ref =
1592 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1593
1594 let mut new_ast = self.program.ast.clone();
1595 let mut segment_ids_edited = AhashIndexSet::with_capacity_and_hasher(edit_segments.len(), Default::default());
1596
1597 for segment in edit_segments {
1599 segment_ids_edited.insert(segment.id);
1600 match segment.ctor {
1601 SegmentCtor::Point(ctor) => self
1602 .edit_point(&mut new_ast, sketch, segment.id, ctor)
1603 .map_err(KclErrorWithOutputs::no_outputs)?,
1604 SegmentCtor::Line(ctor) => self
1605 .edit_line(&mut new_ast, sketch, segment.id, ctor)
1606 .map_err(KclErrorWithOutputs::no_outputs)?,
1607 SegmentCtor::Arc(ctor) => self
1608 .edit_arc(&mut new_ast, sketch, segment.id, ctor)
1609 .map_err(KclErrorWithOutputs::no_outputs)?,
1610 SegmentCtor::Circle(ctor) => self
1611 .edit_circle(&mut new_ast, sketch, segment.id, ctor)
1612 .map_err(KclErrorWithOutputs::no_outputs)?,
1613 SegmentCtor::ControlPointSpline(ctor) => self
1614 .edit_control_point_spline(&mut new_ast, sketch, segment.id, ctor)
1615 .map_err(KclErrorWithOutputs::no_outputs)?,
1616 }
1617 }
1618
1619 for constraint in add_constraints {
1621 match constraint {
1622 Constraint::Coincident(coincident) => {
1623 self.add_coincident(sketch, coincident, &mut new_ast)
1624 .await
1625 .map_err(KclErrorWithOutputs::no_outputs)?;
1626 }
1627 Constraint::Distance(distance) => {
1628 self.add_distance(sketch, distance, &mut new_ast)
1629 .await
1630 .map_err(KclErrorWithOutputs::no_outputs)?;
1631 }
1632 Constraint::EqualRadius(equal_radius) => {
1633 self.add_equal_radius(sketch, equal_radius, &mut new_ast)
1634 .await
1635 .map_err(KclErrorWithOutputs::no_outputs)?;
1636 }
1637 Constraint::Fixed(fixed) => {
1638 self.add_fixed_constraints(sketch, fixed.points, &mut new_ast)
1639 .await
1640 .map_err(KclErrorWithOutputs::no_outputs)?;
1641 }
1642 Constraint::HorizontalDistance(distance) => {
1643 self.add_horizontal_distance(sketch, distance, &mut new_ast)
1644 .await
1645 .map_err(KclErrorWithOutputs::no_outputs)?;
1646 }
1647 Constraint::VerticalDistance(distance) => {
1648 self.add_vertical_distance(sketch, distance, &mut new_ast)
1649 .await
1650 .map_err(KclErrorWithOutputs::no_outputs)?;
1651 }
1652 Constraint::Horizontal(horizontal) => {
1653 self.add_horizontal(sketch, horizontal, &mut new_ast)
1654 .await
1655 .map_err(KclErrorWithOutputs::no_outputs)?;
1656 }
1657 Constraint::LinesEqualLength(lines_equal_length) => {
1658 self.add_lines_equal_length(sketch, lines_equal_length, &mut new_ast)
1659 .await
1660 .map_err(KclErrorWithOutputs::no_outputs)?;
1661 }
1662 Constraint::Midpoint(midpoint) => {
1663 self.add_midpoint(sketch, midpoint, &mut new_ast)
1664 .await
1665 .map_err(KclErrorWithOutputs::no_outputs)?;
1666 }
1667 Constraint::Parallel(parallel) => {
1668 self.add_parallel(sketch, parallel, &mut new_ast)
1669 .await
1670 .map_err(KclErrorWithOutputs::no_outputs)?;
1671 }
1672 Constraint::Perpendicular(perpendicular) => {
1673 self.add_perpendicular(sketch, perpendicular, &mut new_ast)
1674 .await
1675 .map_err(KclErrorWithOutputs::no_outputs)?;
1676 }
1677 Constraint::Vertical(vertical) => {
1678 self.add_vertical(sketch, vertical, &mut new_ast)
1679 .await
1680 .map_err(KclErrorWithOutputs::no_outputs)?;
1681 }
1682 Constraint::Diameter(diameter) => {
1683 self.add_diameter(sketch, diameter, &mut new_ast)
1684 .await
1685 .map_err(KclErrorWithOutputs::no_outputs)?;
1686 }
1687 Constraint::Radius(radius) => {
1688 self.add_radius(sketch, radius, &mut new_ast)
1689 .await
1690 .map_err(KclErrorWithOutputs::no_outputs)?;
1691 }
1692 Constraint::Symmetric(symmetric) => {
1693 self.add_symmetric(sketch, symmetric, &mut new_ast)
1694 .await
1695 .map_err(KclErrorWithOutputs::no_outputs)?;
1696 }
1697 Constraint::Angle(angle) => {
1698 self.add_angle(sketch, angle, &mut new_ast)
1699 .await
1700 .map_err(KclErrorWithOutputs::no_outputs)?;
1701 }
1702 Constraint::Tangent(tangent) => {
1703 self.add_tangent(sketch, tangent, &mut new_ast)
1704 .await
1705 .map_err(KclErrorWithOutputs::no_outputs)?;
1706 }
1707 }
1708 }
1709
1710 let constraint_ids_set = delete_constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
1712
1713 let has_constraint_deletions = !constraint_ids_set.is_empty();
1714 for constraint_id in constraint_ids_set {
1715 self.delete_constraint(&mut new_ast, sketch, constraint_id)
1716 .map_err(KclErrorWithOutputs::no_outputs)?;
1717 }
1718
1719 let (source_delta, mut scene_graph_delta) = self
1723 .execute_after_edit(
1724 ctx,
1725 sketch,
1726 sketch_block_ref,
1727 &mut new_ast,
1728 ExecuteAfterEditOptions {
1729 segment_ids_edited,
1730 edit_kind: EditDeleteKind::Edit,
1731 commit_solved_initial_guesses: true,
1732 },
1733 )
1734 .await?;
1735
1736 if has_constraint_deletions {
1739 scene_graph_delta.invalidates_ids = true;
1740 }
1741
1742 Ok((source_delta, scene_graph_delta))
1743 }
1744
1745 async fn batch_tail_cut_operations(
1746 &mut self,
1747 ctx: &ExecutorContext,
1748 _version: Version,
1749 sketch: ObjectId,
1750 edit_segments: Vec<ExistingSegmentCtor>,
1751 add_constraints: Vec<Constraint>,
1752 delete_constraint_ids: Vec<ObjectId>,
1753 additional_edited_segment_ids: Vec<ObjectId>,
1754 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1755 let sketch_block_ref =
1756 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1757
1758 let mut new_ast = self.program.ast.clone();
1759 let mut segment_ids_edited = AhashIndexSet::with_capacity_and_hasher(edit_segments.len(), Default::default());
1760
1761 for segment in edit_segments {
1763 segment_ids_edited.insert(segment.id);
1764 match segment.ctor {
1765 SegmentCtor::Point(ctor) => self
1766 .edit_point(&mut new_ast, sketch, segment.id, ctor)
1767 .map_err(KclErrorWithOutputs::no_outputs)?,
1768 SegmentCtor::Line(ctor) => self
1769 .edit_line(&mut new_ast, sketch, segment.id, ctor)
1770 .map_err(KclErrorWithOutputs::no_outputs)?,
1771 SegmentCtor::Arc(ctor) => self
1772 .edit_arc(&mut new_ast, sketch, segment.id, ctor)
1773 .map_err(KclErrorWithOutputs::no_outputs)?,
1774 SegmentCtor::Circle(ctor) => self
1775 .edit_circle(&mut new_ast, sketch, segment.id, ctor)
1776 .map_err(KclErrorWithOutputs::no_outputs)?,
1777 SegmentCtor::ControlPointSpline(ctor) => self
1778 .edit_control_point_spline(&mut new_ast, sketch, segment.id, ctor)
1779 .map_err(KclErrorWithOutputs::no_outputs)?,
1780 }
1781 }
1782
1783 segment_ids_edited.extend(additional_edited_segment_ids);
1784
1785 for constraint in add_constraints {
1787 match constraint {
1788 Constraint::Coincident(coincident) => {
1789 self.add_coincident(sketch, coincident, &mut new_ast)
1790 .await
1791 .map_err(KclErrorWithOutputs::no_outputs)?;
1792 }
1793 other => {
1794 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1795 "unsupported constraint in tail cut batch: {other:?}"
1796 ))));
1797 }
1798 }
1799 }
1800
1801 let constraint_ids_set = delete_constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
1803
1804 let has_constraint_deletions = !constraint_ids_set.is_empty();
1805 for constraint_id in constraint_ids_set {
1806 self.delete_constraint(&mut new_ast, sketch, constraint_id)
1807 .map_err(KclErrorWithOutputs::no_outputs)?;
1808 }
1809
1810 let (source_delta, mut scene_graph_delta) = self
1814 .execute_after_edit(
1815 ctx,
1816 sketch,
1817 sketch_block_ref,
1818 &mut new_ast,
1819 ExecuteAfterEditOptions {
1820 segment_ids_edited,
1821 edit_kind: EditDeleteKind::Edit,
1822 commit_solved_initial_guesses: true,
1823 },
1824 )
1825 .await?;
1826
1827 if has_constraint_deletions {
1830 scene_graph_delta.invalidates_ids = true;
1831 }
1832
1833 Ok((source_delta, scene_graph_delta))
1834 }
1835}
1836
1837impl FrontendState {
1838 pub async fn hack_set_program(&mut self, ctx: &ExecutorContext, program: Program) -> ExecResult<SetProgramOutcome> {
1839 self.program = program.clone();
1840
1841 self.point_freedom_cache.clear();
1852 match ctx.run_with_caching(program).await {
1853 Ok(outcome) => {
1854 let outcome = self.update_state_after_exec(outcome, true);
1855 let checkpoint_id = self
1856 .create_sketch_checkpoint(outcome.clone())
1857 .await
1858 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
1859 Ok(SetProgramOutcome::Success {
1860 scene_graph: Box::new(self.scene_graph_for_ui()),
1861 exec_outcome: Box::new(outcome),
1862 checkpoint_id: Some(checkpoint_id),
1863 })
1864 }
1865 Err(mut err) => {
1866 let outcome = self.exec_outcome_from_exec_error(err.clone())?;
1869 self.update_state_after_exec(outcome, true);
1870 err.scene_graph = Some(self.scene_graph_for_ui());
1871 Ok(SetProgramOutcome::ExecFailure { error: Box::new(err) })
1872 }
1873 }
1874 }
1875
1876 pub async fn engine_execute(
1879 &mut self,
1880 ctx: &ExecutorContext,
1881 program: Program,
1882 ) -> Result<SceneGraphDelta, KclErrorWithOutputs> {
1883 self.program = program.clone();
1884
1885 self.point_freedom_cache.clear();
1889 match ctx.run_with_caching(program).await {
1890 Ok(outcome) => {
1891 let outcome = self.update_state_after_exec(outcome, true);
1892 Ok(SceneGraphDelta {
1893 new_graph: self.scene_graph_for_ui(),
1894 exec_outcome: outcome,
1895 new_objects: Default::default(),
1897 invalidates_ids: Default::default(),
1899 })
1900 }
1901 Err(mut err) => {
1902 let outcome = self.exec_outcome_from_exec_error(err.clone())?;
1904 self.update_state_after_exec(outcome, true);
1905 err.scene_graph = Some(self.scene_graph_for_ui());
1906 Err(err)
1907 }
1908 }
1909 }
1910
1911 fn exec_outcome_from_exec_error(&self, err: KclErrorWithOutputs) -> Result<ExecOutcome, KclErrorWithOutputs> {
1912 if matches!(err.error, KclError::EngineHangup { .. }) {
1913 return Err(err);
1917 }
1918
1919 let KclErrorWithOutputs {
1920 error,
1921 mut non_fatal,
1922 variables,
1923 operations,
1924 artifact_graph,
1925 scene_objects,
1926 source_range_to_object,
1927 var_solutions,
1928 refactor_metadata,
1929 filenames,
1930 default_planes,
1931 ..
1932 } = err;
1933
1934 if let Some(source_range) = error.source_ranges().first() {
1935 non_fatal.push(CompilationIssue::fatal(*source_range, error.get_message()));
1936 } else {
1937 non_fatal.push(CompilationIssue::fatal(SourceRange::synthetic(), error.get_message()));
1938 }
1939
1940 Ok(ExecOutcome {
1941 variables,
1942 filenames,
1943 operations,
1944 artifact_graph,
1945 scene_objects,
1946 source_range_to_object,
1947 var_solutions,
1948 refactor_metadata,
1949 issues: non_fatal,
1950 default_planes,
1951 })
1952 }
1953
1954 async fn add_point(
1955 &mut self,
1956 ctx: &ExecutorContext,
1957 sketch: ObjectId,
1958 ctor: PointCtor,
1959 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1960 let at_ast = to_ast_point2d(&ctor.position)
1962 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1963 let point_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
1964 callee: ast::Node::no_src(ast_sketch2_name(POINT_FN)),
1965 unlabeled: None,
1966 arguments: vec![ast::LabeledArg {
1967 label: Some(ast::Identifier::new(POINT_AT_PARAM)),
1968 arg: at_ast,
1969 }],
1970 digest: None,
1971 non_code_meta: Default::default(),
1972 })));
1973
1974 let sketch_id = sketch;
1976 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
1977 #[cfg(target_arch = "wasm32")]
1978 web_sys::console::error_1(
1979 &format!(
1980 "Sketch not found; sketch_id={sketch_id:?}, self.scene_graph.objects={:#?}",
1981 self.scene_graph.objects
1982 )
1983 .into(),
1984 );
1985 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
1986 })?;
1987 let ObjectKind::Sketch(_) = &sketch_object.kind else {
1988 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1989 "Object is not a sketch, it is {}",
1990 sketch_object.kind.human_friendly_kind_with_article(),
1991 ))));
1992 };
1993 let mut new_ast = self.program.ast.clone();
1995 let (sketch_block_ref, _) = self
1996 .mutate_ast(
1997 &mut new_ast,
1998 sketch_id,
1999 AstMutateCommand::AddSketchBlockExprStmt { expr: point_ast },
2000 )
2001 .map_err(KclErrorWithOutputs::no_outputs)?;
2002 let new_source = source_from_ast(&new_ast);
2004 let new_program = parse_frontend_mutation_source(
2006 &new_source,
2007 "Error parsing KCL source after adding point",
2008 "No AST produced after adding point",
2009 )?;
2010
2011 let point_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2012 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2013 "Source range of point not found in sketch block: {sketch_block_ref:?}; {err:?}"
2014 )))
2015 })?;
2016
2017 self.program = new_program.clone();
2019
2020 let mut truncated_program = new_program;
2022 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2023 .map_err(KclErrorWithOutputs::no_outputs)?;
2024
2025 let outcome = ctx
2027 .run_mock(
2028 &truncated_program,
2029 &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2030 )
2031 .await?;
2032
2033 let new_object_ids = {
2034 let make_err =
2035 |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2036 let segment_id = outcome
2037 .source_range_to_object
2038 .get(&point_node_ref.range)
2039 .copied()
2040 .ok_or_else(|| make_err(format!("Source range of point not found: {point_node_ref:?}")))?;
2041 let segment_object = outcome
2042 .scene_objects
2043 .get(segment_id.0)
2044 .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2045 let ObjectKind::Segment { segment } = &segment_object.kind else {
2046 return Err(make_err(format!(
2047 "Object is not a segment, it is {}",
2048 segment_object.kind.human_friendly_kind_with_article()
2049 )));
2050 };
2051 let Segment::Point(_) = segment else {
2052 return Err(make_err(format!(
2053 "Segment is not a point, it is {}",
2054 segment.human_friendly_kind_with_article()
2055 )));
2056 };
2057 vec![segment_id]
2058 };
2059 let src_delta = SourceDelta { text: new_source };
2060 let outcome = self.update_state_after_exec(outcome, false);
2062 let scene_graph_delta = SceneGraphDelta {
2063 new_graph: self.scene_graph_for_ui(),
2064 invalidates_ids: false,
2065 new_objects: new_object_ids,
2066 exec_outcome: outcome,
2067 };
2068 Ok((src_delta, scene_graph_delta))
2069 }
2070
2071 async fn add_line(
2072 &mut self,
2073 ctx: &ExecutorContext,
2074 sketch: ObjectId,
2075 ctor: LineCtor,
2076 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2077 let start_ast = to_ast_point2d(&ctor.start)
2079 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2080 let end_ast = to_ast_point2d(&ctor.end)
2081 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2082 let mut arguments = vec![
2083 ast::LabeledArg {
2084 label: Some(ast::Identifier::new(LINE_START_PARAM)),
2085 arg: start_ast,
2086 },
2087 ast::LabeledArg {
2088 label: Some(ast::Identifier::new(LINE_END_PARAM)),
2089 arg: end_ast,
2090 },
2091 ];
2092 if ctor.construction == Some(true) {
2094 arguments.push(ast::LabeledArg {
2095 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2096 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
2097 value: ast::LiteralValue::Bool(true),
2098 raw: "true".to_string(),
2099 digest: None,
2100 }))),
2101 });
2102 }
2103 let line_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
2104 callee: ast::Node::no_src(ast_sketch2_name(LINE_FN)),
2105 unlabeled: None,
2106 arguments,
2107 digest: None,
2108 non_code_meta: Default::default(),
2109 })));
2110
2111 let sketch_id = sketch;
2113 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2114 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2115 })?;
2116 let ObjectKind::Sketch(_) = &sketch_object.kind else {
2117 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2118 "Object is not a sketch, it is {}",
2119 sketch_object.kind.human_friendly_kind_with_article(),
2120 ))));
2121 };
2122 let mut new_ast = self.program.ast.clone();
2124 let (sketch_block_ref, _) = self
2125 .mutate_ast(
2126 &mut new_ast,
2127 sketch_id,
2128 AstMutateCommand::AddSketchBlockExprStmt { expr: line_ast },
2129 )
2130 .map_err(KclErrorWithOutputs::no_outputs)?;
2131 let new_source = source_from_ast(&new_ast);
2133 let new_program = parse_frontend_mutation_source(
2135 &new_source,
2136 "Error parsing KCL source after adding line",
2137 "No AST produced after adding line",
2138 )?;
2139
2140 let line_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2141 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2142 "Source range of line not found in sketch block: {sketch_block_ref:?}; {err:?}"
2143 )))
2144 })?;
2145
2146 self.program = new_program.clone();
2148
2149 let mut truncated_program = new_program;
2151 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2152 .map_err(KclErrorWithOutputs::no_outputs)?;
2153
2154 let outcome = ctx
2156 .run_mock(
2157 &truncated_program,
2158 &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2159 )
2160 .await?;
2161
2162 let new_object_ids = {
2163 let make_err =
2164 |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2165 let segment_id = outcome
2166 .source_range_to_object
2167 .get(&line_node_ref.range)
2168 .copied()
2169 .ok_or_else(|| make_err(format!("Source range of line not found: {line_node_ref:?}")))?;
2170 let segment_object = outcome
2171 .scene_object_by_id(segment_id)
2172 .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2173 let ObjectKind::Segment { segment } = &segment_object.kind else {
2174 return Err(make_err(format!(
2175 "Object is not a segment, it is {}",
2176 segment_object.kind.human_friendly_kind_with_article()
2177 )));
2178 };
2179 let Segment::Line(line) = segment else {
2180 return Err(make_err(format!(
2181 "Segment is not a line, it is {}",
2182 segment.human_friendly_kind_with_article()
2183 )));
2184 };
2185 vec![line.start, line.end, segment_id]
2186 };
2187 let src_delta = SourceDelta { text: new_source };
2188 let outcome = self.update_state_after_exec(outcome, false);
2190 let scene_graph_delta = SceneGraphDelta {
2191 new_graph: self.scene_graph_for_ui(),
2192 invalidates_ids: false,
2193 new_objects: new_object_ids,
2194 exec_outcome: outcome,
2195 };
2196 Ok((src_delta, scene_graph_delta))
2197 }
2198
2199 async fn add_arc(
2200 &mut self,
2201 ctx: &ExecutorContext,
2202 sketch: ObjectId,
2203 ctor: ArcCtor,
2204 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2205 let start_ast = to_ast_point2d(&ctor.start)
2207 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2208 let end_ast = to_ast_point2d(&ctor.end)
2209 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2210 let center_ast = to_ast_point2d(&ctor.center)
2211 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2212 let mut arguments = vec![
2213 ast::LabeledArg {
2214 label: Some(ast::Identifier::new(ARC_START_PARAM)),
2215 arg: start_ast,
2216 },
2217 ast::LabeledArg {
2218 label: Some(ast::Identifier::new(ARC_END_PARAM)),
2219 arg: end_ast,
2220 },
2221 ast::LabeledArg {
2222 label: Some(ast::Identifier::new(ARC_CENTER_PARAM)),
2223 arg: center_ast,
2224 },
2225 ];
2226 if ctor.construction == Some(true) {
2228 arguments.push(ast::LabeledArg {
2229 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2230 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
2231 value: ast::LiteralValue::Bool(true),
2232 raw: "true".to_string(),
2233 digest: None,
2234 }))),
2235 });
2236 }
2237 let arc_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
2238 callee: ast::Node::no_src(ast_sketch2_name(ARC_FN)),
2239 unlabeled: None,
2240 arguments,
2241 digest: None,
2242 non_code_meta: Default::default(),
2243 })));
2244
2245 let sketch_id = sketch;
2247 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2248 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2249 })?;
2250 let ObjectKind::Sketch(_) = &sketch_object.kind else {
2251 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2252 "Object is not a sketch, it is {}",
2253 sketch_object.kind.human_friendly_kind_with_article(),
2254 ))));
2255 };
2256 let mut new_ast = self.program.ast.clone();
2258 let (sketch_block_ref, _) = self
2259 .mutate_ast(
2260 &mut new_ast,
2261 sketch_id,
2262 AstMutateCommand::AddSketchBlockExprStmt { expr: arc_ast },
2263 )
2264 .map_err(KclErrorWithOutputs::no_outputs)?;
2265 let new_source = source_from_ast(&new_ast);
2267 let new_program = parse_frontend_mutation_source(
2269 &new_source,
2270 "Error parsing KCL source after adding arc",
2271 "No AST produced after adding arc",
2272 )?;
2273
2274 let arc_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2275 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2276 "Source range of arc not found in sketch block: {sketch_block_ref:?}; {err:?}"
2277 )))
2278 })?;
2279
2280 self.program = new_program.clone();
2282
2283 let mut truncated_program = new_program;
2285 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2286 .map_err(KclErrorWithOutputs::no_outputs)?;
2287
2288 let outcome = ctx
2290 .run_mock(
2291 &truncated_program,
2292 &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2293 )
2294 .await?;
2295
2296 let new_object_ids = {
2297 let make_err =
2298 |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2299 let segment_id = outcome
2300 .source_range_to_object
2301 .get(&arc_node_ref.range)
2302 .copied()
2303 .ok_or_else(|| make_err(format!("Source range of arc not found: {arc_node_ref:?}")))?;
2304 let segment_object = outcome
2305 .scene_objects
2306 .get(segment_id.0)
2307 .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2308 let ObjectKind::Segment { segment } = &segment_object.kind else {
2309 return Err(make_err(format!(
2310 "Object is not a segment, it is {}",
2311 segment_object.kind.human_friendly_kind_with_article()
2312 )));
2313 };
2314 let Segment::Arc(arc) = segment else {
2315 return Err(make_err(format!(
2316 "Segment is not an arc, it is {}",
2317 segment.human_friendly_kind_with_article()
2318 )));
2319 };
2320 vec![arc.start, arc.end, arc.center, segment_id]
2321 };
2322 let src_delta = SourceDelta { text: new_source };
2323 let outcome = self.update_state_after_exec(outcome, false);
2325 let scene_graph_delta = SceneGraphDelta {
2326 new_graph: self.scene_graph_for_ui(),
2327 invalidates_ids: false,
2328 new_objects: new_object_ids,
2329 exec_outcome: outcome,
2330 };
2331 Ok((src_delta, scene_graph_delta))
2332 }
2333
2334 async fn add_circle(
2335 &mut self,
2336 ctx: &ExecutorContext,
2337 sketch: ObjectId,
2338 ctor: CircleCtor,
2339 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2340 let start_ast = to_ast_point2d(&ctor.start)
2342 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2343 let center_ast = to_ast_point2d(&ctor.center)
2344 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2345 let mut arguments = vec![
2346 ast::LabeledArg {
2347 label: Some(ast::Identifier::new(CIRCLE_START_PARAM)),
2348 arg: start_ast,
2349 },
2350 ast::LabeledArg {
2351 label: Some(ast::Identifier::new(CIRCLE_CENTER_PARAM)),
2352 arg: center_ast,
2353 },
2354 ];
2355 if ctor.construction == Some(true) {
2357 arguments.push(ast::LabeledArg {
2358 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2359 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
2360 value: ast::LiteralValue::Bool(true),
2361 raw: "true".to_string(),
2362 digest: None,
2363 }))),
2364 });
2365 }
2366 let circle_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
2367 callee: ast::Node::no_src(ast_sketch2_name(CIRCLE_FN)),
2368 unlabeled: None,
2369 arguments,
2370 digest: None,
2371 non_code_meta: Default::default(),
2372 })));
2373
2374 let sketch_id = sketch;
2376 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2377 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2378 })?;
2379 let ObjectKind::Sketch(_) = &sketch_object.kind else {
2380 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2381 "Object is not a sketch, it is {}",
2382 sketch_object.kind.human_friendly_kind_with_article(),
2383 ))));
2384 };
2385 let mut new_ast = self.program.ast.clone();
2387 let (sketch_block_ref, _) = self
2388 .mutate_ast(
2389 &mut new_ast,
2390 sketch_id,
2391 AstMutateCommand::AddSketchBlockVarDecl {
2392 prefix: CIRCLE_VARIABLE.to_owned(),
2393 expr: circle_ast,
2394 },
2395 )
2396 .map_err(KclErrorWithOutputs::no_outputs)?;
2397 let new_source = source_from_ast(&new_ast);
2399 let new_program = parse_frontend_mutation_source(
2401 &new_source,
2402 "Error parsing KCL source after adding circle",
2403 "No AST produced after adding circle",
2404 )?;
2405
2406 let circle_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2407 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2408 "Source range of circle not found in sketch block: {sketch_block_ref:?}; {err:?}"
2409 )))
2410 })?;
2411
2412 self.program = new_program.clone();
2414
2415 let mut truncated_program = new_program;
2417 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2418 .map_err(KclErrorWithOutputs::no_outputs)?;
2419
2420 let outcome = ctx
2422 .run_mock(
2423 &truncated_program,
2424 &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2425 )
2426 .await?;
2427
2428 let new_object_ids = {
2429 let make_err =
2430 |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2431 let segment_id = outcome
2432 .source_range_to_object
2433 .get(&circle_node_ref.range)
2434 .copied()
2435 .ok_or_else(|| make_err(format!("Source range of circle not found: {circle_node_ref:?}")))?;
2436 let segment_object = outcome
2437 .scene_objects
2438 .get(segment_id.0)
2439 .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2440 let ObjectKind::Segment { segment } = &segment_object.kind else {
2441 return Err(make_err(format!(
2442 "Object is not a segment, it is {}",
2443 segment_object.kind.human_friendly_kind_with_article()
2444 )));
2445 };
2446 let Segment::Circle(circle) = segment else {
2447 return Err(make_err(format!(
2448 "Segment is not a circle, it is {}",
2449 segment.human_friendly_kind_with_article()
2450 )));
2451 };
2452 vec![circle.start, circle.center, segment_id]
2453 };
2454 let src_delta = SourceDelta { text: new_source };
2455 let outcome = self.update_state_after_exec(outcome, false);
2457 let scene_graph_delta = SceneGraphDelta {
2458 new_graph: self.scene_graph_for_ui(),
2459 invalidates_ids: false,
2460 new_objects: new_object_ids,
2461 exec_outcome: outcome,
2462 };
2463 Ok((src_delta, scene_graph_delta))
2464 }
2465
2466 async fn add_control_point_spline(
2467 &mut self,
2468 ctx: &ExecutorContext,
2469 sketch: ObjectId,
2470 ctor: ControlPointSplineCtor,
2471 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2472 let new_program = ensure_control_point_spline_experimental_features(&self.program)
2473 .map_err(KclErrorWithOutputs::no_outputs)?;
2474
2475 let points_ast = to_ast_point2d_array(&ctor.points)
2476 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2477 let mut arguments = vec![ast::LabeledArg {
2478 label: Some(ast::Identifier::new(CONTROL_POINT_SPLINE_POINTS_PARAM)),
2479 arg: points_ast,
2480 }];
2481 if ctor.construction == Some(true) {
2482 arguments.push(ast::LabeledArg {
2483 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2484 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
2485 value: ast::LiteralValue::Bool(true),
2486 raw: "true".to_string(),
2487 digest: None,
2488 }))),
2489 });
2490 }
2491 let spline_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
2492 callee: ast::Node::no_src(ast_sketch2_name(CONTROL_POINT_SPLINE_FN)),
2493 unlabeled: None,
2494 arguments,
2495 digest: None,
2496 non_code_meta: Default::default(),
2497 })));
2498
2499 let sketch_object = self.scene_graph.objects.get(sketch.0).ok_or_else(|| {
2500 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2501 })?;
2502 let ObjectKind::Sketch(_) = &sketch_object.kind else {
2503 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2504 "Object is not a sketch, it is {}",
2505 sketch_object.kind.human_friendly_kind_with_article(),
2506 ))));
2507 };
2508
2509 let mut new_ast = new_program.ast.clone();
2510 let (sketch_block_ref, _) = self
2511 .mutate_ast(
2512 &mut new_ast,
2513 sketch,
2514 AstMutateCommand::AddSketchBlockExprStmt { expr: spline_ast },
2515 )
2516 .map_err(KclErrorWithOutputs::no_outputs)?;
2517 let new_source = source_from_ast(&new_ast);
2518 let new_program = parse_frontend_mutation_source(
2519 &new_source,
2520 "Error parsing KCL source after adding controlPointSpline",
2521 "No AST produced after adding controlPointSpline",
2522 )?;
2523
2524 let spline_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2525 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2526 "Source range of controlPointSpline not found in sketch block: {sketch_block_ref:?}; {err:?}"
2527 )))
2528 })?;
2529
2530 self.program = new_program.clone();
2531
2532 let mut truncated_program = new_program;
2533 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2534 .map_err(KclErrorWithOutputs::no_outputs)?;
2535
2536 let outcome = ctx
2537 .run_mock(
2538 &truncated_program,
2539 &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2540 )
2541 .await?;
2542
2543 let new_object_ids = {
2544 let make_err =
2545 |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2546 let segment_id = outcome
2547 .source_range_to_object
2548 .get(&spline_node_ref.range)
2549 .copied()
2550 .ok_or_else(|| {
2551 make_err(format!(
2552 "Source range of controlPointSpline not found: {spline_node_ref:?}"
2553 ))
2554 })?;
2555 let segment_object = outcome
2556 .scene_objects
2557 .get(segment_id.0)
2558 .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2559 let ObjectKind::Segment { segment } = &segment_object.kind else {
2560 return Err(make_err(format!(
2561 "Object is not a segment, it is {}",
2562 segment_object.kind.human_friendly_kind_with_article()
2563 )));
2564 };
2565 let Segment::ControlPointSpline(spline) = segment else {
2566 return Err(make_err(format!(
2567 "Segment is not a control point spline, it is {}",
2568 segment.human_friendly_kind_with_article()
2569 )));
2570 };
2571
2572 let mut ids = outcome
2573 .scene_objects
2574 .iter()
2575 .filter_map(|obj| match &obj.kind {
2576 ObjectKind::Segment {
2577 segment: Segment::Line(line),
2578 } if line.owner == Some(segment_id) => Some(obj.id),
2579 _ => None,
2580 })
2581 .collect::<Vec<_>>();
2582 ids.extend(spline.controls.clone());
2583 ids.push(segment_id);
2584 ids
2585 };
2586 let src_delta = SourceDelta { text: new_source };
2587 let outcome = self.update_state_after_exec(outcome, false);
2588 let scene_graph_delta = SceneGraphDelta {
2589 new_graph: self.scene_graph_for_ui(),
2590 invalidates_ids: false,
2591 new_objects: new_object_ids,
2592 exec_outcome: outcome,
2593 };
2594 Ok((src_delta, scene_graph_delta))
2595 }
2596
2597 fn edit_point(
2598 &mut self,
2599 new_ast: &mut ast::Node<ast::Program>,
2600 sketch: ObjectId,
2601 point: ObjectId,
2602 ctor: PointCtor,
2603 ) -> Result<(), KclError> {
2604 let new_at_ast = to_ast_point2d(&ctor.position).map_err(|err| KclError::refactor(err.to_string()))?;
2606
2607 let sketch_id = sketch;
2609 let sketch_object = self
2610 .scene_graph
2611 .objects
2612 .get(sketch_id.0)
2613 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2614 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2615 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2616 };
2617 sketch.segments.iter().find(|o| **o == point).ok_or_else(|| {
2618 KclError::refactor(format!("Point not found in sketch: point={point:?}, sketch={sketch:?}"))
2619 })?;
2620 let point_id = point;
2622 let point_object = self
2623 .scene_graph
2624 .objects
2625 .get(point_id.0)
2626 .ok_or_else(|| KclError::refactor(format!("Point not found in scene graph: point={point:?}")))?;
2627 let ObjectKind::Segment {
2628 segment: Segment::Point(point),
2629 } = &point_object.kind
2630 else {
2631 return Err(KclError::refactor(format!(
2632 "Object is not a point segment: {point_object:?}"
2633 )));
2634 };
2635
2636 if let Some(owner_id) = point.owner {
2638 let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
2639 KclError::refactor(format!(
2640 "Internal: Owner of point not found in scene graph: owner={owner_id:?}",
2641 ))
2642 })?;
2643 let ObjectKind::Segment { segment } = &owner_object.kind else {
2644 return Err(KclError::refactor(format!(
2645 "Internal: Owner of point is not a segment, but found {}",
2646 owner_object.kind.human_friendly_kind_with_article()
2647 )));
2648 };
2649
2650 if let Segment::Line(line) = segment {
2652 let SegmentCtor::Line(line_ctor) = &line.ctor else {
2653 return Err(KclError::refactor(format!(
2654 "Internal: Owner of point does not have line ctor, but found {}",
2655 line.ctor.human_friendly_kind_with_article()
2656 )));
2657 };
2658 let mut line_ctor = line_ctor.clone();
2659 if line.start == point_id {
2661 line_ctor.start = ctor.position;
2662 } else if line.end == point_id {
2663 line_ctor.end = ctor.position;
2664 } else {
2665 return Err(KclError::refactor(format!(
2666 "Internal: Point is not part of owner's line segment: point={point_id:?}, line={owner_id:?}"
2667 )));
2668 }
2669 return self.edit_line(new_ast, sketch_id, owner_id, line_ctor);
2670 }
2671
2672 if let Segment::Arc(arc) = segment {
2674 let SegmentCtor::Arc(arc_ctor) = &arc.ctor else {
2675 return Err(KclError::refactor(format!(
2676 "Internal: Owner of point does not have arc ctor, but found {}",
2677 arc.ctor.human_friendly_kind_with_article()
2678 )));
2679 };
2680 let mut arc_ctor = arc_ctor.clone();
2681 if arc.center == point_id {
2683 arc_ctor.center = ctor.position;
2684 } else if arc.start == point_id {
2685 arc_ctor.start = ctor.position;
2686 } else if arc.end == point_id {
2687 arc_ctor.end = ctor.position;
2688 } else {
2689 return Err(KclError::refactor(format!(
2690 "Internal: Point is not part of owner's arc segment: point={point_id:?}, arc={owner_id:?}"
2691 )));
2692 }
2693 return self.edit_arc(new_ast, sketch_id, owner_id, arc_ctor);
2694 }
2695
2696 if let Segment::Circle(circle) = segment {
2698 let SegmentCtor::Circle(circle_ctor) = &circle.ctor else {
2699 return Err(KclError::refactor(format!(
2700 "Internal: Owner of point does not have circle ctor, but found {}",
2701 circle.ctor.human_friendly_kind_with_article()
2702 )));
2703 };
2704 let mut circle_ctor = circle_ctor.clone();
2705 if circle.center == point_id {
2706 circle_ctor.center = ctor.position;
2707 } else if circle.start == point_id {
2708 circle_ctor.start = ctor.position;
2709 } else {
2710 return Err(KclError::refactor(format!(
2711 "Internal: Point is not part of owner's circle segment: point={point_id:?}, circle={owner_id:?}"
2712 )));
2713 }
2714 return self.edit_circle(new_ast, sketch_id, owner_id, circle_ctor);
2715 }
2716
2717 if let Segment::ControlPointSpline(spline) = segment {
2718 let SegmentCtor::ControlPointSpline(spline_ctor) = &spline.ctor else {
2719 return Err(KclError::refactor(format!(
2720 "Internal: Owner of point does not have controlPointSpline ctor, but found {}",
2721 spline.ctor.human_friendly_kind_with_article()
2722 )));
2723 };
2724 let mut spline_ctor = spline_ctor.clone();
2725 let Some(control_index) = spline.controls.iter().position(|id| *id == point_id) else {
2726 return Err(KclError::refactor(format!(
2727 "Internal: Point is not part of owner's controlPointSpline segment: point={point_id:?}, spline={owner_id:?}"
2728 )));
2729 };
2730 spline_ctor.points[control_index] = ctor.position;
2731 return self.edit_control_point_spline(new_ast, sketch_id, owner_id, spline_ctor);
2732 }
2733
2734 }
2737
2738 self.mutate_ast(new_ast, point_id, AstMutateCommand::EditPoint { at: new_at_ast })?;
2740 Ok(())
2741 }
2742
2743 fn edit_line(
2744 &mut self,
2745 new_ast: &mut ast::Node<ast::Program>,
2746 sketch: ObjectId,
2747 line: ObjectId,
2748 ctor: LineCtor,
2749 ) -> Result<(), KclError> {
2750 let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2752 let new_end_ast = to_ast_point2d(&ctor.end).map_err(|err| KclError::refactor(err.to_string()))?;
2753
2754 let sketch_id = sketch;
2756 let sketch_object = self
2757 .scene_graph
2758 .objects
2759 .get(sketch_id.0)
2760 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2761 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2762 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2763 };
2764 sketch
2765 .segments
2766 .iter()
2767 .find(|o| **o == line)
2768 .ok_or_else(|| KclError::refactor(format!("Line not found in sketch: line={line:?}, sketch={sketch:?}")))?;
2769 let line_id = line;
2771 let line_object = self
2772 .scene_graph
2773 .objects
2774 .get(line_id.0)
2775 .ok_or_else(|| KclError::refactor(format!("Line not found in scene graph: line={line:?}")))?;
2776 let ObjectKind::Segment { .. } = &line_object.kind else {
2777 let kind = line_object.kind.human_friendly_kind_with_article();
2778 return Err(KclError::refactor(format!(
2779 "This constraint only works on Segments, but you selected {kind}"
2780 )));
2781 };
2782
2783 self.mutate_ast(
2785 new_ast,
2786 line_id,
2787 AstMutateCommand::EditLine {
2788 start: new_start_ast,
2789 end: new_end_ast,
2790 construction: ctor.construction,
2791 },
2792 )?;
2793 Ok(())
2794 }
2795
2796 fn edit_arc(
2797 &mut self,
2798 new_ast: &mut ast::Node<ast::Program>,
2799 sketch: ObjectId,
2800 arc: ObjectId,
2801 ctor: ArcCtor,
2802 ) -> Result<(), KclError> {
2803 let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2805 let new_end_ast = to_ast_point2d(&ctor.end).map_err(|err| KclError::refactor(err.to_string()))?;
2806 let new_center_ast = to_ast_point2d(&ctor.center).map_err(|err| KclError::refactor(err.to_string()))?;
2807
2808 let sketch_id = sketch;
2810 let sketch_object = self
2811 .scene_graph
2812 .objects
2813 .get(sketch_id.0)
2814 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2815 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2816 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2817 };
2818 sketch
2819 .segments
2820 .iter()
2821 .find(|o| **o == arc)
2822 .ok_or_else(|| KclError::refactor(format!("Arc not found in sketch: arc={arc:?}, sketch={sketch:?}")))?;
2823 let arc_id = arc;
2825 let arc_object = self
2826 .scene_graph
2827 .objects
2828 .get(arc_id.0)
2829 .ok_or_else(|| KclError::refactor(format!("Arc not found in scene graph: arc={arc:?}")))?;
2830 let ObjectKind::Segment { .. } = &arc_object.kind else {
2831 return Err(KclError::refactor(format!("Object is not a segment: {arc_object:?}")));
2832 };
2833
2834 self.mutate_ast(
2836 new_ast,
2837 arc_id,
2838 AstMutateCommand::EditArc {
2839 start: new_start_ast,
2840 end: new_end_ast,
2841 center: new_center_ast,
2842 construction: ctor.construction,
2843 },
2844 )?;
2845 Ok(())
2846 }
2847
2848 fn edit_circle(
2849 &mut self,
2850 new_ast: &mut ast::Node<ast::Program>,
2851 sketch: ObjectId,
2852 circle: ObjectId,
2853 ctor: CircleCtor,
2854 ) -> Result<(), KclError> {
2855 let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2857 let new_center_ast = to_ast_point2d(&ctor.center).map_err(|err| KclError::refactor(err.to_string()))?;
2858
2859 let sketch_id = sketch;
2861 let sketch_object = self
2862 .scene_graph
2863 .objects
2864 .get(sketch_id.0)
2865 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2866 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2867 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2868 };
2869 sketch.segments.iter().find(|o| **o == circle).ok_or_else(|| {
2870 KclError::refactor(format!(
2871 "Circle not found in sketch: circle={circle:?}, sketch={sketch:?}"
2872 ))
2873 })?;
2874 let circle_id = circle;
2876 let circle_object = self
2877 .scene_graph
2878 .objects
2879 .get(circle_id.0)
2880 .ok_or_else(|| KclError::refactor(format!("Circle not found in scene graph: circle={circle:?}")))?;
2881 let ObjectKind::Segment { .. } = &circle_object.kind else {
2882 return Err(KclError::refactor(format!(
2883 "Object is not a segment: {circle_object:?}"
2884 )));
2885 };
2886
2887 self.mutate_ast(
2889 new_ast,
2890 circle_id,
2891 AstMutateCommand::EditCircle {
2892 start: new_start_ast,
2893 center: new_center_ast,
2894 construction: ctor.construction,
2895 },
2896 )?;
2897 Ok(())
2898 }
2899
2900 fn edit_control_point_spline(
2901 &mut self,
2902 new_ast: &mut ast::Node<ast::Program>,
2903 sketch: ObjectId,
2904 spline: ObjectId,
2905 ctor: ControlPointSplineCtor,
2906 ) -> Result<(), KclError> {
2907 let points_ast = to_ast_point2d_array(&ctor.points).map_err(|err| KclError::refactor(err.to_string()))?;
2908
2909 let sketch_object = self
2910 .scene_graph
2911 .objects
2912 .get(sketch.0)
2913 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2914 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2915 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2916 };
2917 sketch.segments.iter().find(|o| **o == spline).ok_or_else(|| {
2918 KclError::refactor(format!(
2919 "Control point spline not found in sketch: spline={spline:?}, sketch={sketch:?}"
2920 ))
2921 })?;
2922
2923 let spline_object =
2924 self.scene_graph.objects.get(spline.0).ok_or_else(|| {
2925 KclError::refactor(format!("Control point spline not found in scene graph: {spline:?}"))
2926 })?;
2927 let ObjectKind::Segment { .. } = &spline_object.kind else {
2928 return Err(KclError::refactor(format!(
2929 "Object is not a segment: {spline_object:?}"
2930 )));
2931 };
2932
2933 self.mutate_ast(
2934 new_ast,
2935 spline,
2936 AstMutateCommand::EditControlPointSpline {
2937 points: points_ast,
2938 construction: ctor.construction,
2939 },
2940 )?;
2941 Ok(())
2942 }
2943
2944 fn delete_segment(
2945 &mut self,
2946 new_ast: &mut ast::Node<ast::Program>,
2947 sketch: ObjectId,
2948 segment_id: ObjectId,
2949 ) -> Result<(), KclError> {
2950 let sketch_id = sketch;
2952 let sketch_object = self
2953 .scene_graph
2954 .objects
2955 .get(sketch_id.0)
2956 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2957 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2958 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2959 };
2960 sketch.segments.iter().find(|o| **o == segment_id).ok_or_else(|| {
2961 KclError::refactor(format!(
2962 "Segment not found in sketch: segment={segment_id:?}, sketch={sketch:?}"
2963 ))
2964 })?;
2965 let segment_object =
2967 self.scene_graph.objects.get(segment_id.0).ok_or_else(|| {
2968 KclError::refactor(format!("Segment not found in scene graph: segment={segment_id:?}"))
2969 })?;
2970 let ObjectKind::Segment { .. } = &segment_object.kind else {
2971 return Err(KclError::refactor(format!(
2972 "Object is not a segment, it is {}",
2973 segment_object.kind.human_friendly_kind_with_article()
2974 )));
2975 };
2976
2977 self.mutate_ast(new_ast, segment_id, AstMutateCommand::DeleteNode)?;
2979 Ok(())
2980 }
2981
2982 fn delete_constraint(
2983 &mut self,
2984 new_ast: &mut ast::Node<ast::Program>,
2985 sketch: ObjectId,
2986 constraint_id: ObjectId,
2987 ) -> Result<(), KclError> {
2988 let sketch_id = sketch;
2990 let sketch_object = self
2991 .scene_graph
2992 .objects
2993 .get(sketch_id.0)
2994 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2995 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2996 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2997 };
2998 sketch
2999 .constraints
3000 .iter()
3001 .find(|o| **o == constraint_id)
3002 .ok_or_else(|| {
3003 KclError::refactor(format!(
3004 "Constraint not found in sketch: constraint={constraint_id:?}, sketch={sketch:?}"
3005 ))
3006 })?;
3007 let constraint_object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
3009 KclError::refactor(format!(
3010 "Constraint not found in scene graph: constraint={constraint_id:?}"
3011 ))
3012 })?;
3013 let ObjectKind::Constraint { .. } = &constraint_object.kind else {
3014 return Err(KclError::refactor(format!(
3015 "Object is not a constraint, it is {}",
3016 constraint_object.kind.human_friendly_kind_with_article()
3017 )));
3018 };
3019
3020 self.mutate_ast(new_ast, constraint_id, AstMutateCommand::DeleteNode)?;
3022 Ok(())
3023 }
3024
3025 fn edit_coincident_constraint(
3026 &mut self,
3027 new_ast: &mut ast::Node<ast::Program>,
3028 constraint_id: ObjectId,
3029 segments: Vec<ConstraintSegment>,
3030 ) -> Result<(), KclError> {
3031 if segments.len() < 2 {
3032 return Err(KclError::refactor(format!(
3033 "Coincident constraint must have at least 2 inputs, got {}",
3034 segments.len()
3035 )));
3036 }
3037
3038 let segment_asts = segments
3039 .iter()
3040 .map(|segment| self.coincident_segment_to_ast(segment, new_ast))
3041 .collect::<Result<Vec<_>, _>>()?;
3042
3043 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
3044 elements: segment_asts,
3045 digest: None,
3046 non_code_meta: Default::default(),
3047 })));
3048
3049 self.mutate_ast(
3050 new_ast,
3051 constraint_id,
3052 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3053 )?;
3054 Ok(())
3055 }
3056
3057 fn edit_horizontal_points_constraint(
3058 &mut self,
3059 new_ast: &mut ast::Node<ast::Program>,
3060 constraint_id: ObjectId,
3061 points: Vec<ConstraintSegment>,
3062 ) -> Result<(), KclError> {
3063 self.edit_axis_points_constraint(new_ast, constraint_id, points, "Horizontal")
3064 }
3065
3066 fn edit_vertical_points_constraint(
3067 &mut self,
3068 new_ast: &mut ast::Node<ast::Program>,
3069 constraint_id: ObjectId,
3070 points: Vec<ConstraintSegment>,
3071 ) -> Result<(), KclError> {
3072 self.edit_axis_points_constraint(new_ast, constraint_id, points, "Vertical")
3073 }
3074
3075 fn edit_axis_points_constraint(
3076 &mut self,
3077 new_ast: &mut ast::Node<ast::Program>,
3078 constraint_id: ObjectId,
3079 points: Vec<ConstraintSegment>,
3080 constraint_name: &str,
3081 ) -> Result<(), KclError> {
3082 if points.len() < 2 {
3083 return Err(KclError::refactor(format!(
3084 "{constraint_name} points constraint must have at least 2 points, got {}",
3085 points.len()
3086 )));
3087 }
3088
3089 let point_asts = points
3090 .iter()
3091 .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
3092 .collect::<Result<Vec<_>, _>>()?;
3093
3094 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
3095 elements: point_asts,
3096 digest: None,
3097 non_code_meta: Default::default(),
3098 })));
3099
3100 self.mutate_ast(
3101 new_ast,
3102 constraint_id,
3103 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3104 )?;
3105 Ok(())
3106 }
3107
3108 fn edit_equal_length_constraint(
3110 &mut self,
3111 new_ast: &mut ast::Node<ast::Program>,
3112 constraint_id: ObjectId,
3113 lines: Vec<ObjectId>,
3114 ) -> Result<(), KclError> {
3115 if lines.len() < 2 {
3116 return Err(KclError::refactor(format!(
3117 "Lines equal length constraint must have at least 2 lines, got {}",
3118 lines.len()
3119 )));
3120 }
3121
3122 let line_asts = lines
3123 .iter()
3124 .map(|line_id| {
3125 let line_object = self
3126 .scene_graph
3127 .objects
3128 .get(line_id.0)
3129 .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3130 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3131 let kind = line_object.kind.human_friendly_kind_with_article();
3132 return Err(KclError::refactor(format!(
3133 "This constraint only works on Segments, but you selected {kind}"
3134 )));
3135 };
3136 let Segment::Line(_) = line_segment else {
3137 let kind = line_segment.human_friendly_kind_with_article();
3138 return Err(KclError::refactor(format!(
3139 "Only lines can be made equal length, but you selected {kind}"
3140 )));
3141 };
3142
3143 get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)
3144 })
3145 .collect::<Result<Vec<_>, _>>()?;
3146
3147 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
3148 elements: line_asts,
3149 digest: None,
3150 non_code_meta: Default::default(),
3151 })));
3152
3153 self.mutate_ast(
3154 new_ast,
3155 constraint_id,
3156 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3157 )?;
3158 Ok(())
3159 }
3160
3161 fn edit_parallel_constraint(
3163 &mut self,
3164 new_ast: &mut ast::Node<ast::Program>,
3165 constraint_id: ObjectId,
3166 lines: Vec<ObjectId>,
3167 ) -> Result<(), KclError> {
3168 if lines.len() < 2 {
3169 return Err(KclError::refactor(format!(
3170 "Parallel constraint must have at least 2 lines, got {}",
3171 lines.len()
3172 )));
3173 }
3174
3175 let line_asts = lines
3176 .iter()
3177 .map(|line_id| {
3178 let line_object = self
3179 .scene_graph
3180 .objects
3181 .get(line_id.0)
3182 .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3183 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3184 let kind = line_object.kind.human_friendly_kind_with_article();
3185 return Err(KclError::refactor(format!(
3186 "This constraint only works on Segments, but you selected {kind}"
3187 )));
3188 };
3189 let Segment::Line(_) = line_segment else {
3190 let kind = line_segment.human_friendly_kind_with_article();
3191 return Err(KclError::refactor(format!(
3192 "Only lines can be made parallel, but you selected {kind}"
3193 )));
3194 };
3195
3196 get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)
3197 })
3198 .collect::<Result<Vec<_>, _>>()?;
3199
3200 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
3201 elements: line_asts,
3202 digest: None,
3203 non_code_meta: Default::default(),
3204 })));
3205
3206 self.mutate_ast(
3207 new_ast,
3208 constraint_id,
3209 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3210 )?;
3211 Ok(())
3212 }
3213
3214 fn edit_equal_radius_constraint(
3216 &mut self,
3217 new_ast: &mut ast::Node<ast::Program>,
3218 constraint_id: ObjectId,
3219 input: Vec<ObjectId>,
3220 ) -> Result<(), KclError> {
3221 if input.len() < 2 {
3222 return Err(KclError::refactor(format!(
3223 "equalRadius constraint must have at least 2 segments, got {}",
3224 input.len()
3225 )));
3226 }
3227
3228 let input_asts = input
3229 .iter()
3230 .map(|segment_id| self.equal_radius_segment_id_to_ast_reference(*segment_id, new_ast))
3231 .collect::<Result<Vec<_>, _>>()?;
3232
3233 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
3234 elements: input_asts,
3235 digest: None,
3236 non_code_meta: Default::default(),
3237 })));
3238
3239 self.mutate_ast(
3240 new_ast,
3241 constraint_id,
3242 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3243 )?;
3244 Ok(())
3245 }
3246
3247 async fn execute_after_edit(
3248 &mut self,
3249 ctx: &ExecutorContext,
3250 sketch: ObjectId,
3251 sketch_block_ref: AstNodeRef,
3252 new_ast: &mut ast::Node<ast::Program>,
3253 options: ExecuteAfterEditOptions,
3254 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
3255 let ExecuteAfterEditOptions {
3256 segment_ids_edited,
3257 edit_kind,
3258 commit_solved_initial_guesses,
3259 } = options;
3260
3261 let new_source = source_from_ast(new_ast);
3263 let new_program = parse_frontend_mutation_source(
3265 &new_source,
3266 "Error parsing KCL source after editing",
3267 "No AST produced after editing",
3268 )?;
3269
3270 let is_delete = edit_kind.is_delete();
3272 let truncated_program = {
3273 let mut truncated_program = new_program.clone();
3274 only_sketch_block(
3275 &mut truncated_program.ast,
3276 &sketch_block_ref,
3277 edit_kind.to_change_kind(),
3278 )
3279 .map_err(KclErrorWithOutputs::no_outputs)?;
3280 truncated_program
3281 };
3282
3283 let drag_anchors = self.next_segment_drag_anchors.take().unwrap_or_default();
3285 let mock_config = MockConfig {
3286 sketch_block_id: Some(sketch),
3287 freedom_analysis: is_delete,
3288 segment_ids_edited: segment_ids_edited.clone(),
3289 drag_anchors,
3290 ..Default::default()
3291 };
3292 let outcome = ctx.run_mock(&truncated_program, &mock_config).await?;
3293
3294 self.program = new_program;
3296
3297 let outcome = self.update_state_after_exec(outcome, is_delete);
3299
3300 let src_delta = if commit_solved_initial_guesses {
3301 self.commit_var_solutions_to_program(&outcome, "editing")?
3302 } else {
3303 SourceDelta { text: new_source }
3304 };
3305 let scene_graph_delta = SceneGraphDelta {
3306 new_graph: self.scene_graph_for_ui(),
3307 invalidates_ids: is_delete,
3308 new_objects: Vec::new(),
3309 exec_outcome: outcome,
3310 };
3311 Ok((src_delta, scene_graph_delta))
3312 }
3313
3314 async fn execute_after_delete_sketch(
3315 &mut self,
3316 ctx: &ExecutorContext,
3317 new_ast: &mut ast::Node<ast::Program>,
3318 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
3319 let new_source = source_from_ast(new_ast);
3321 let new_program = parse_frontend_mutation_source(
3323 &new_source,
3324 "Error parsing KCL source after editing",
3325 "No AST produced after editing",
3326 )?;
3327
3328 self.program = new_program.clone();
3330
3331 let outcome = ctx.run_with_caching(new_program).await?;
3337 let freedom_analysis_ran = true;
3338
3339 let outcome = self.update_state_after_exec(outcome, freedom_analysis_ran);
3340
3341 let src_delta = SourceDelta { text: new_source };
3342 let scene_graph_delta = SceneGraphDelta {
3343 new_graph: self.scene_graph_for_ui(),
3344 invalidates_ids: true,
3345 new_objects: Vec::new(),
3346 exec_outcome: outcome,
3347 };
3348 Ok((src_delta, scene_graph_delta))
3349 }
3350
3351 fn point_id_to_ast_reference(
3356 &self,
3357 point_id: ObjectId,
3358 new_ast: &mut ast::Node<ast::Program>,
3359 ) -> Result<ast::Expr, KclError> {
3360 let point_object = self
3361 .scene_graph
3362 .objects
3363 .get(point_id.0)
3364 .ok_or_else(|| KclError::refactor(format!("Point not found: {point_id:?}")))?;
3365 let ObjectKind::Segment { segment: point_segment } = &point_object.kind else {
3366 return Err(KclError::refactor(format!("Object is not a segment: {point_object:?}")));
3367 };
3368 let Segment::Point(point) = point_segment else {
3369 return Err(KclError::refactor(format!(
3370 "Only points are currently supported: {point_object:?}"
3371 )));
3372 };
3373
3374 if let Some(owner_id) = point.owner {
3375 let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
3376 KclError::refactor(format!(
3377 "Owner of point not found in scene graph: point={point_id:?}, owner={owner_id:?}"
3378 ))
3379 })?;
3380 let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind else {
3381 return Err(KclError::refactor(format!(
3382 "Owner of point is not a segment, but found {}",
3383 owner_object.kind.human_friendly_kind_with_article()
3384 )));
3385 };
3386
3387 match owner_segment {
3388 Segment::Line(line) => {
3389 let property = if line.start == point_id {
3390 LINE_PROPERTY_START
3391 } else if line.end == point_id {
3392 LINE_PROPERTY_END
3393 } else {
3394 return Err(KclError::refactor(format!(
3395 "Internal: Point is not part of owner's line segment: point={point_id:?}, line={owner_id:?}"
3396 )));
3397 };
3398 get_or_insert_ast_reference(new_ast, &owner_object.source, LINE_VARIABLE, Some(property))
3399 }
3400 Segment::Arc(arc) => {
3401 let property = if arc.start == point_id {
3402 ARC_PROPERTY_START
3403 } else if arc.end == point_id {
3404 ARC_PROPERTY_END
3405 } else if arc.center == point_id {
3406 ARC_PROPERTY_CENTER
3407 } else {
3408 return Err(KclError::refactor(format!(
3409 "Internal: Point is not part of owner's arc segment: point={point_id:?}, arc={owner_id:?}"
3410 )));
3411 };
3412 get_or_insert_ast_reference(new_ast, &owner_object.source, ARC_VARIABLE, Some(property))
3413 }
3414 Segment::Circle(circle) => {
3415 let property = if circle.start == point_id {
3416 CIRCLE_PROPERTY_START
3417 } else if circle.center == point_id {
3418 CIRCLE_PROPERTY_CENTER
3419 } else {
3420 return Err(KclError::refactor(format!(
3421 "Internal: Point is not part of owner's circle segment: point={point_id:?}, circle={owner_id:?}"
3422 )));
3423 };
3424 get_or_insert_ast_reference(new_ast, &owner_object.source, CIRCLE_VARIABLE, Some(property))
3425 }
3426 Segment::ControlPointSpline(spline) => {
3427 let Some(index) = spline.controls.iter().position(|id| *id == point_id) else {
3428 return Err(KclError::refactor(format!(
3429 "Internal: Point is not part of owner's controlPointSpline segment: point={point_id:?}, spline={owner_id:?}"
3430 )));
3431 };
3432 let owner_expr =
3433 get_or_insert_ast_reference(new_ast, &owner_object.source, CONTROL_POINT_SPLINE_FN, None)?;
3434 let controls_expr = create_member_expression(owner_expr, CONTROL_POINT_SPLINE_PROPERTY_CONTROLS);
3435 Ok(create_index_expression(controls_expr, index))
3436 }
3437 _ => Err(KclError::refactor(format!(
3438 "Internal: Owner of point is not a supported segment type for constraints: {owner_segment:?}"
3439 ))),
3440 }
3441 } else {
3442 get_or_insert_ast_reference(new_ast, &point_object.source, "point", None)
3444 }
3445 }
3446
3447 fn line_id_to_ast_reference(
3448 &self,
3449 line_id: ObjectId,
3450 new_ast: &mut ast::Node<ast::Program>,
3451 ) -> Result<ast::Expr, KclError> {
3452 let line_object = self
3453 .scene_graph
3454 .objects
3455 .get(line_id.0)
3456 .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3457 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3458 return Err(KclError::refactor(format!("Object is not a segment: {line_object:?}")));
3459 };
3460 let Segment::Line(line) = line_segment else {
3461 return Err(KclError::refactor(format!(
3462 "Only lines are currently supported: {line_object:?}"
3463 )));
3464 };
3465
3466 if let Some(owner_id) = line.owner {
3467 let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
3468 KclError::refactor(format!(
3469 "Owner of line not found in scene graph: line={line_id:?}, owner={owner_id:?}"
3470 ))
3471 })?;
3472 let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind else {
3473 return Err(KclError::refactor(format!(
3474 "Owner of line is not a segment, but found {}",
3475 owner_object.kind.human_friendly_kind_with_article()
3476 )));
3477 };
3478
3479 match owner_segment {
3480 Segment::ControlPointSpline(spline) => {
3481 let Some(index) = spline
3482 .controls
3483 .windows(2)
3484 .position(|window| window[0] == line.start && window[1] == line.end)
3485 else {
3486 return Err(KclError::refactor(format!(
3487 "Internal: Line is not part of owner's controlPointSpline segment: line={line_id:?}, spline={owner_id:?}"
3488 )));
3489 };
3490 let owner_expr =
3491 get_or_insert_ast_reference(new_ast, &owner_object.source, CONTROL_POINT_SPLINE_FN, None)?;
3492 let edges_expr = create_member_expression(owner_expr, CONTROL_POINT_SPLINE_PROPERTY_EDGES);
3493 Ok(create_index_expression(edges_expr, index))
3494 }
3495 _ => Err(KclError::refactor(format!(
3496 "Internal: Owner of line is not a supported segment type for constraints: {owner_segment:?}"
3497 ))),
3498 }
3499 } else {
3500 get_or_insert_ast_reference(new_ast, &line_object.source, "line", None)
3501 }
3502 }
3503
3504 fn coincident_segment_to_ast(
3505 &self,
3506 segment: &ConstraintSegment,
3507 new_ast: &mut ast::Node<ast::Program>,
3508 ) -> Result<ast::Expr, KclError> {
3509 match segment {
3510 ConstraintSegment::Origin(_) => Ok(ast_name_expr("ORIGIN".to_owned())),
3511 ConstraintSegment::Segment(segment_id) => self.segment_id_to_constraint_ast_reference(*segment_id, new_ast),
3512 }
3513 }
3514
3515 fn segment_id_to_constraint_ast_reference(
3516 &self,
3517 segment_id: ObjectId,
3518 new_ast: &mut ast::Node<ast::Program>,
3519 ) -> Result<ast::Expr, KclError> {
3520 let segment_object = self
3521 .scene_graph
3522 .objects
3523 .get(segment_id.0)
3524 .ok_or_else(|| KclError::refactor(format!("Object not found: {segment_id:?}")))?;
3525 let ObjectKind::Segment { segment } = &segment_object.kind else {
3526 return Err(KclError::refactor(format!(
3527 "Object is not a segment, it is {}",
3528 segment_object.kind.human_friendly_kind_with_article()
3529 )));
3530 };
3531
3532 match segment {
3533 Segment::Point(_) => self.point_id_to_ast_reference(segment_id, new_ast),
3534 Segment::Line(_) => self.line_id_to_ast_reference(segment_id, new_ast),
3535 Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, "arc", None),
3536 Segment::Circle(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, CIRCLE_VARIABLE, None),
3537 Segment::ControlPointSpline(_) => {
3538 get_or_insert_ast_reference(new_ast, &segment_object.source, CONTROL_POINT_SPLINE_FN, None)
3539 }
3540 }
3541 }
3542
3543 fn axis_constraint_segment_to_ast(
3544 &self,
3545 segment: &ConstraintSegment,
3546 new_ast: &mut ast::Node<ast::Program>,
3547 ) -> Result<ast::Expr, KclError> {
3548 match segment {
3549 ConstraintSegment::Origin(_) => Ok(ast_name_expr("ORIGIN".to_owned())),
3550 ConstraintSegment::Segment(point_id) => self.point_id_to_ast_reference(*point_id, new_ast),
3551 }
3552 }
3553
3554 async fn add_coincident(
3555 &mut self,
3556 sketch: ObjectId,
3557 coincident: Coincident,
3558 new_ast: &mut ast::Node<ast::Program>,
3559 ) -> Result<AstNodeRef, KclError> {
3560 let sketch_id = sketch;
3561 for segment in &coincident.segments {
3562 let ConstraintSegment::Segment(segment_id) = segment else {
3563 continue;
3564 };
3565 let Some(segment_object) = self.scene_graph.objects.get(segment_id.0) else {
3566 continue;
3567 };
3568 if matches!(
3569 segment_object.kind,
3570 ObjectKind::Segment {
3571 segment: Segment::ControlPointSpline(_)
3572 }
3573 ) {
3574 return Err(KclError::refactor(
3575 "Coincident with a full controlPointSpline is not supported yet. Constrain a control point or spline edge instead."
3576 .to_owned(),
3577 ));
3578 }
3579 }
3580 let segment_asts = coincident
3581 .segments
3582 .iter()
3583 .map(|segment| self.coincident_segment_to_ast(segment, new_ast))
3584 .collect::<Result<Vec<_>, _>>()?;
3585 if segment_asts.len() < 2 {
3586 return Err(KclError::refactor(format!(
3587 "Coincident constraint must have at least 2 inputs, got {}",
3588 segment_asts.len()
3589 )));
3590 }
3591
3592 let coincident_ast = create_coincident_ast(segment_asts);
3594
3595 let (sketch_block_ref, _) = self.mutate_ast(
3597 new_ast,
3598 sketch_id,
3599 AstMutateCommand::AddSketchBlockExprStmt { expr: coincident_ast },
3600 )?;
3601 Ok(sketch_block_ref)
3602 }
3603
3604 async fn add_distance(
3605 &mut self,
3606 sketch: ObjectId,
3607 distance: Distance,
3608 new_ast: &mut ast::Node<ast::Program>,
3609 ) -> Result<AstNodeRef, KclError> {
3610 let sketch_id = sketch;
3611 let [pt0_ast, pt1_ast] = match distance.points.as_slice() {
3612 [pt0, pt1] => [
3613 self.coincident_segment_to_ast(pt0, new_ast)?,
3614 self.coincident_segment_to_ast(pt1, new_ast)?,
3615 ],
3616 _ => {
3617 return Err(KclError::refactor(format!(
3618 "Distance constraint must have exactly 2 points, got {}",
3619 distance.points.len()
3620 )));
3621 }
3622 };
3623
3624 let arguments = match &distance.label_position {
3625 Some(label_position) => vec![ast::LabeledArg {
3626 label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
3627 arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
3628 }],
3629 None => Default::default(),
3630 };
3631
3632 let distance_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3634 callee: ast::Node::no_src(ast_sketch2_name(DISTANCE_FN)),
3635 unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
3636 ast::ArrayExpression {
3637 elements: vec![pt0_ast, pt1_ast],
3638 digest: None,
3639 non_code_meta: Default::default(),
3640 },
3641 )))),
3642 arguments,
3643 digest: None,
3644 non_code_meta: Default::default(),
3645 })));
3646 let distance_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
3647 left: distance_call_ast,
3648 operator: ast::BinaryOperator::Eq,
3649 right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
3650 value: ast::LiteralValue::Number {
3651 value: distance.distance.value,
3652 suffix: distance.distance.units,
3653 },
3654 raw: format_number_literal(distance.distance.value, distance.distance.units, None).map_err(|_| {
3655 KclError::refactor(format!(
3656 "Could not format numeric suffix: {:?}",
3657 distance.distance.units
3658 ))
3659 })?,
3660 digest: None,
3661 }))),
3662 digest: None,
3663 })));
3664
3665 let (sketch_block_ref, _) = self.mutate_ast(
3667 new_ast,
3668 sketch_id,
3669 AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
3670 )?;
3671 Ok(sketch_block_ref)
3672 }
3673
3674 async fn add_angle(
3675 &mut self,
3676 sketch: ObjectId,
3677 angle: Angle,
3678 new_ast: &mut ast::Node<ast::Program>,
3679 ) -> Result<AstNodeRef, KclError> {
3680 let &[l0_id, l1_id] = angle.lines.as_slice() else {
3681 return Err(KclError::refactor(format!(
3682 "Angle constraint must have exactly 2 lines, got {}",
3683 angle.lines.len()
3684 )));
3685 };
3686 let sketch_id = sketch;
3687
3688 let line0_object = self
3690 .scene_graph
3691 .objects
3692 .get(l0_id.0)
3693 .ok_or_else(|| KclError::refactor(format!("Line not found: {l0_id:?}")))?;
3694 let ObjectKind::Segment { segment: line0_segment } = &line0_object.kind else {
3695 return Err(KclError::refactor(format!("Object is not a segment: {line0_object:?}")));
3696 };
3697 let Segment::Line(_) = line0_segment else {
3698 return Err(KclError::refactor(format!(
3699 "Only lines can be constrained to meet at an angle: {line0_object:?}",
3700 )));
3701 };
3702 let l0_ast = self.line_id_to_ast_reference(l0_id, new_ast)?;
3703
3704 let line1_object = self
3705 .scene_graph
3706 .objects
3707 .get(l1_id.0)
3708 .ok_or_else(|| KclError::refactor(format!("Line not found: {l1_id:?}")))?;
3709 let ObjectKind::Segment { segment: line1_segment } = &line1_object.kind else {
3710 return Err(KclError::refactor(format!("Object is not a segment: {line1_object:?}")));
3711 };
3712 let Segment::Line(_) = line1_segment else {
3713 return Err(KclError::refactor(format!(
3714 "Only lines can be constrained to meet at an angle: {line1_object:?}",
3715 )));
3716 };
3717 let l1_ast = self.line_id_to_ast_reference(l1_id, new_ast)?;
3718
3719 let angle_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3721 callee: ast::Node::no_src(ast_sketch2_name(ANGLE_FN)),
3722 unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
3723 ast::ArrayExpression {
3724 elements: vec![l0_ast, l1_ast],
3725 digest: None,
3726 non_code_meta: Default::default(),
3727 },
3728 )))),
3729 arguments: Default::default(),
3730 digest: None,
3731 non_code_meta: Default::default(),
3732 })));
3733 let angle_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
3734 left: angle_call_ast,
3735 operator: ast::BinaryOperator::Eq,
3736 right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
3737 value: ast::LiteralValue::Number {
3738 value: angle.angle.value,
3739 suffix: angle.angle.units,
3740 },
3741 raw: format_number_literal(angle.angle.value, angle.angle.units, None).map_err(|_| {
3742 KclError::refactor(format!("Could not format numeric suffix: {:?}", angle.angle.units))
3743 })?,
3744 digest: None,
3745 }))),
3746 digest: None,
3747 })));
3748
3749 let (sketch_block_ref, _) = self.mutate_ast(
3751 new_ast,
3752 sketch_id,
3753 AstMutateCommand::AddSketchBlockExprStmt { expr: angle_ast },
3754 )?;
3755 Ok(sketch_block_ref)
3756 }
3757
3758 async fn add_tangent(
3759 &mut self,
3760 sketch: ObjectId,
3761 tangent: Tangent,
3762 new_ast: &mut ast::Node<ast::Program>,
3763 ) -> Result<AstNodeRef, KclError> {
3764 let &[seg0_id, seg1_id] = tangent.input.as_slice() else {
3765 return Err(KclError::refactor(format!(
3766 "Tangent constraint must have exactly 2 segments, got {}",
3767 tangent.input.len()
3768 )));
3769 };
3770 let sketch_id = sketch;
3771
3772 let seg0_object = self
3773 .scene_graph
3774 .objects
3775 .get(seg0_id.0)
3776 .ok_or_else(|| KclError::refactor(format!("Segment not found: {seg0_id:?}")))?;
3777 let ObjectKind::Segment { segment: seg0_segment } = &seg0_object.kind else {
3778 return Err(KclError::refactor(format!("Object is not a segment: {seg0_object:?}")));
3779 };
3780 let seg0_ast = match seg0_segment {
3781 Segment::Line(_) | Segment::Arc(_) | Segment::Circle(_) => {
3782 self.segment_id_to_constraint_ast_reference(seg0_id, new_ast)?
3783 }
3784 _ => {
3785 return Err(KclError::refactor(format!(
3786 "Tangent supports only line/arc/circle segments for now, got: {seg0_segment:?}"
3787 )));
3788 }
3789 };
3790
3791 let seg1_object = self
3792 .scene_graph
3793 .objects
3794 .get(seg1_id.0)
3795 .ok_or_else(|| KclError::refactor(format!("Segment not found: {seg1_id:?}")))?;
3796 let ObjectKind::Segment { segment: seg1_segment } = &seg1_object.kind else {
3797 return Err(KclError::refactor(format!("Object is not a segment: {seg1_object:?}")));
3798 };
3799 let seg1_ast = match seg1_segment {
3800 Segment::Line(_) | Segment::Arc(_) | Segment::Circle(_) => {
3801 self.segment_id_to_constraint_ast_reference(seg1_id, new_ast)?
3802 }
3803 _ => {
3804 return Err(KclError::refactor(format!(
3805 "Tangent supports only line/arc/circle segments for now, got: {seg1_segment:?}"
3806 )));
3807 }
3808 };
3809
3810 let tangent_ast = create_tangent_ast(seg0_ast, seg1_ast);
3811 let (sketch_block_ref, _) = self.mutate_ast(
3812 new_ast,
3813 sketch_id,
3814 AstMutateCommand::AddSketchBlockExprStmt { expr: tangent_ast },
3815 )?;
3816 Ok(sketch_block_ref)
3817 }
3818
3819 async fn add_symmetric(
3820 &mut self,
3821 sketch: ObjectId,
3822 symmetric: Symmetric,
3823 new_ast: &mut ast::Node<ast::Program>,
3824 ) -> Result<AstNodeRef, KclError> {
3825 let &[input0_id, input1_id] = symmetric.input.as_slice() else {
3826 return Err(KclError::refactor(format!(
3827 "Symmetric constraint must have exactly 2 inputs, got {}",
3828 symmetric.input.len()
3829 )));
3830 };
3831 let sketch_id = sketch;
3832
3833 let input0_ast = self.symmetric_input_id_to_ast_reference(input0_id, new_ast)?;
3834 let input1_ast = self.symmetric_input_id_to_ast_reference(input1_id, new_ast)?;
3835 let axis_ast = self.symmetric_axis_id_to_ast_reference(symmetric.axis, new_ast)?;
3836
3837 let symmetric_ast = create_symmetric_ast(vec![input0_ast, input1_ast], axis_ast);
3838 let (sketch_block_ref, _) = self.mutate_ast(
3839 new_ast,
3840 sketch_id,
3841 AstMutateCommand::AddSketchBlockExprStmt { expr: symmetric_ast },
3842 )?;
3843 Ok(sketch_block_ref)
3844 }
3845
3846 async fn add_midpoint(
3847 &mut self,
3848 sketch: ObjectId,
3849 midpoint: Midpoint,
3850 new_ast: &mut ast::Node<ast::Program>,
3851 ) -> Result<AstNodeRef, KclError> {
3852 let sketch_id = sketch;
3853 let point_ast = self.axis_constraint_segment_to_ast(&midpoint.point, new_ast)?;
3854
3855 let segment_object = self
3856 .scene_graph
3857 .objects
3858 .get(midpoint.segment.0)
3859 .ok_or_else(|| KclError::refactor(format!("Segment not found: {:?}", midpoint.segment)))?;
3860 let ObjectKind::Segment {
3861 segment: midpoint_segment,
3862 } = &segment_object.kind
3863 else {
3864 return Err(KclError::refactor(format!(
3865 "Object must be a segment, but it was {}",
3866 segment_object.kind.human_friendly_kind_with_article()
3867 )));
3868 };
3869 let segment_ast = match midpoint_segment {
3870 Segment::Line(_) => self.line_id_to_ast_reference(midpoint.segment, new_ast)?,
3871 Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, "arc", None)?,
3872 _ => {
3873 return Err(KclError::refactor(format!(
3874 "Midpoint target must be a line or arc segment but it was {}",
3875 midpoint_segment.human_friendly_kind_with_article()
3876 )));
3877 }
3878 };
3879
3880 let midpoint_ast = create_midpoint_ast(segment_ast, point_ast);
3881 let (sketch_block_ref, _) = self.mutate_ast(
3882 new_ast,
3883 sketch_id,
3884 AstMutateCommand::AddSketchBlockExprStmt { expr: midpoint_ast },
3885 )?;
3886 Ok(sketch_block_ref)
3887 }
3888
3889 async fn add_equal_radius(
3890 &mut self,
3891 sketch: ObjectId,
3892 equal_radius: EqualRadius,
3893 new_ast: &mut ast::Node<ast::Program>,
3894 ) -> Result<AstNodeRef, KclError> {
3895 if equal_radius.input.len() < 2 {
3896 return Err(KclError::refactor(format!(
3897 "equalRadius constraint must have at least 2 segments, got {}",
3898 equal_radius.input.len()
3899 )));
3900 }
3901
3902 let sketch_id = sketch;
3903 let input_asts = equal_radius
3904 .input
3905 .iter()
3906 .map(|segment_id| self.equal_radius_segment_id_to_ast_reference(*segment_id, new_ast))
3907 .collect::<Result<Vec<_>, _>>()?;
3908
3909 let equal_radius_ast = create_equal_radius_ast(input_asts);
3910 let (sketch_block_ref, _) = self.mutate_ast(
3911 new_ast,
3912 sketch_id,
3913 AstMutateCommand::AddSketchBlockExprStmt { expr: equal_radius_ast },
3914 )?;
3915 Ok(sketch_block_ref)
3916 }
3917
3918 async fn add_radius(
3919 &mut self,
3920 sketch: ObjectId,
3921 radius: Radius,
3922 new_ast: &mut ast::Node<ast::Program>,
3923 ) -> Result<AstNodeRef, KclError> {
3924 let params = ArcSizeConstraintParams {
3925 points: vec![radius.arc],
3926 function_name: RADIUS_FN,
3927 value: radius.radius.value,
3928 units: radius.radius.units,
3929 label_position: radius.label_position,
3930 constraint_type_name: "Radius",
3931 };
3932 self.add_arc_size_constraint(sketch, params, new_ast).await
3933 }
3934
3935 async fn add_diameter(
3936 &mut self,
3937 sketch: ObjectId,
3938 diameter: Diameter,
3939 new_ast: &mut ast::Node<ast::Program>,
3940 ) -> Result<AstNodeRef, KclError> {
3941 let params = ArcSizeConstraintParams {
3942 points: vec![diameter.arc],
3943 function_name: DIAMETER_FN,
3944 value: diameter.diameter.value,
3945 units: diameter.diameter.units,
3946 label_position: diameter.label_position,
3947 constraint_type_name: "Diameter",
3948 };
3949 self.add_arc_size_constraint(sketch, params, new_ast).await
3950 }
3951
3952 async fn add_fixed_constraints(
3953 &mut self,
3954 sketch: ObjectId,
3955 points: Vec<FixedPoint>,
3956 new_ast: &mut ast::Node<ast::Program>,
3957 ) -> Result<AstNodeRef, KclError> {
3958 let mut sketch_block_ref = None;
3959
3960 for fixed_point in points {
3961 let point_ast = self.point_id_to_ast_reference(fixed_point.point, new_ast)?;
3962 let fixed_ast = create_fixed_point_constraint_ast(point_ast, fixed_point.position)
3963 .map_err(|err| KclError::refactor(err.to_string()))?;
3964
3965 let (sketch_ref, _) = self.mutate_ast(
3966 new_ast,
3967 sketch,
3968 AstMutateCommand::AddSketchBlockExprStmt { expr: fixed_ast },
3969 )?;
3970 sketch_block_ref = Some(sketch_ref);
3971 }
3972
3973 sketch_block_ref.ok_or_else(|| KclError::refactor("Fixed constraint requires at least one point".to_owned()))
3974 }
3975
3976 async fn add_arc_size_constraint(
3977 &mut self,
3978 sketch: ObjectId,
3979 params: ArcSizeConstraintParams,
3980 new_ast: &mut ast::Node<ast::Program>,
3981 ) -> Result<AstNodeRef, KclError> {
3982 let sketch_id = sketch;
3983
3984 if params.points.len() != 1 {
3986 return Err(KclError::refactor(format!(
3987 "{} constraint must have exactly 1 argument (an arc segment), got {}",
3988 params.constraint_type_name,
3989 params.points.len()
3990 )));
3991 }
3992
3993 let arc_id = params.points[0];
3994 let arc_object = self
3995 .scene_graph
3996 .objects
3997 .get(arc_id.0)
3998 .ok_or_else(|| KclError::refactor(format!("Arc segment not found: {arc_id:?}")))?;
3999 let ObjectKind::Segment { segment: arc_segment } = &arc_object.kind else {
4000 return Err(KclError::refactor(format!("Object is not a segment: {arc_object:?}")));
4001 };
4002 let ref_type = match arc_segment {
4003 Segment::Arc(_) => ARC_VARIABLE,
4004 Segment::Circle(_) => CIRCLE_VARIABLE,
4005 _ => {
4006 return Err(KclError::refactor(format!(
4007 "{} constraint argument must be an arc or circle segment, got: {arc_segment:?}",
4008 params.constraint_type_name
4009 )));
4010 }
4011 };
4012 let arc_ast = get_or_insert_ast_reference(new_ast, &arc_object.source, ref_type, None)?;
4014 let arguments = match ¶ms.label_position {
4015 Some(label_position) => vec![ast::LabeledArg {
4016 label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
4017 arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
4018 }],
4019 None => Default::default(),
4020 };
4021
4022 let call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
4024 callee: ast::Node::no_src(ast_sketch2_name(params.function_name)),
4025 unlabeled: Some(arc_ast),
4026 arguments,
4027 digest: None,
4028 non_code_meta: Default::default(),
4029 })));
4030 let constraint_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
4031 left: call_ast,
4032 operator: ast::BinaryOperator::Eq,
4033 right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
4034 value: ast::LiteralValue::Number {
4035 value: params.value,
4036 suffix: params.units,
4037 },
4038 raw: format_number_literal(params.value, params.units, None)
4039 .map_err(|_| KclError::refactor(format!("Could not format numeric suffix: {:?}", params.units)))?,
4040 digest: None,
4041 }))),
4042 digest: None,
4043 })));
4044
4045 let (sketch_block_ref, _) = self.mutate_ast(
4047 new_ast,
4048 sketch_id,
4049 AstMutateCommand::AddSketchBlockExprStmt { expr: constraint_ast },
4050 )?;
4051 Ok(sketch_block_ref)
4052 }
4053
4054 async fn add_horizontal_distance(
4055 &mut self,
4056 sketch: ObjectId,
4057 distance: Distance,
4058 new_ast: &mut ast::Node<ast::Program>,
4059 ) -> Result<AstNodeRef, KclError> {
4060 let sketch_id = sketch;
4061 let [pt0_ast, pt1_ast] = match distance.points.as_slice() {
4062 [pt0, pt1] => [
4063 self.coincident_segment_to_ast(pt0, new_ast)?,
4064 self.coincident_segment_to_ast(pt1, new_ast)?,
4065 ],
4066 _ => {
4067 return Err(KclError::refactor(format!(
4068 "Horizontal distance constraint must have exactly 2 points, got {}",
4069 distance.points.len()
4070 )));
4071 }
4072 };
4073
4074 let arguments = match &distance.label_position {
4075 Some(label_position) => vec![ast::LabeledArg {
4076 label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
4077 arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
4078 }],
4079 None => Default::default(),
4080 };
4081
4082 let distance_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
4084 callee: ast::Node::no_src(ast_sketch2_name(HORIZONTAL_DISTANCE_FN)),
4085 unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
4086 ast::ArrayExpression {
4087 elements: vec![pt0_ast, pt1_ast],
4088 digest: None,
4089 non_code_meta: Default::default(),
4090 },
4091 )))),
4092 arguments,
4093 digest: None,
4094 non_code_meta: Default::default(),
4095 })));
4096 let distance_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
4097 left: distance_call_ast,
4098 operator: ast::BinaryOperator::Eq,
4099 right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
4100 value: ast::LiteralValue::Number {
4101 value: distance.distance.value,
4102 suffix: distance.distance.units,
4103 },
4104 raw: format_number_literal(distance.distance.value, distance.distance.units, None).map_err(|_| {
4105 KclError::refactor(format!(
4106 "Could not format numeric suffix: {:?}",
4107 distance.distance.units
4108 ))
4109 })?,
4110 digest: None,
4111 }))),
4112 digest: None,
4113 })));
4114
4115 let (sketch_block_ref, _) = self.mutate_ast(
4117 new_ast,
4118 sketch_id,
4119 AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
4120 )?;
4121 Ok(sketch_block_ref)
4122 }
4123
4124 async fn add_vertical_distance(
4125 &mut self,
4126 sketch: ObjectId,
4127 distance: Distance,
4128 new_ast: &mut ast::Node<ast::Program>,
4129 ) -> Result<AstNodeRef, KclError> {
4130 let sketch_id = sketch;
4131 let [pt0_ast, pt1_ast] = match distance.points.as_slice() {
4132 [pt0, pt1] => [
4133 self.coincident_segment_to_ast(pt0, new_ast)?,
4134 self.coincident_segment_to_ast(pt1, new_ast)?,
4135 ],
4136 _ => {
4137 return Err(KclError::refactor(format!(
4138 "Vertical distance constraint must have exactly 2 points, got {}",
4139 distance.points.len()
4140 )));
4141 }
4142 };
4143
4144 let arguments = match &distance.label_position {
4145 Some(label_position) => vec![ast::LabeledArg {
4146 label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
4147 arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
4148 }],
4149 None => Default::default(),
4150 };
4151
4152 let distance_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
4154 callee: ast::Node::no_src(ast_sketch2_name(VERTICAL_DISTANCE_FN)),
4155 unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
4156 ast::ArrayExpression {
4157 elements: vec![pt0_ast, pt1_ast],
4158 digest: None,
4159 non_code_meta: Default::default(),
4160 },
4161 )))),
4162 arguments,
4163 digest: None,
4164 non_code_meta: Default::default(),
4165 })));
4166 let distance_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
4167 left: distance_call_ast,
4168 operator: ast::BinaryOperator::Eq,
4169 right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
4170 value: ast::LiteralValue::Number {
4171 value: distance.distance.value,
4172 suffix: distance.distance.units,
4173 },
4174 raw: format_number_literal(distance.distance.value, distance.distance.units, None).map_err(|_| {
4175 KclError::refactor(format!(
4176 "Could not format numeric suffix: {:?}",
4177 distance.distance.units
4178 ))
4179 })?,
4180 digest: None,
4181 }))),
4182 digest: None,
4183 })));
4184
4185 let (sketch_block_ref, _) = self.mutate_ast(
4187 new_ast,
4188 sketch_id,
4189 AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
4190 )?;
4191 Ok(sketch_block_ref)
4192 }
4193
4194 async fn add_horizontal(
4195 &mut self,
4196 sketch: ObjectId,
4197 horizontal: Horizontal,
4198 new_ast: &mut ast::Node<ast::Program>,
4199 ) -> Result<AstNodeRef, KclError> {
4200 let sketch_id = sketch;
4201
4202 let first_arg_ast = match horizontal {
4204 Horizontal::Line { line } => {
4205 let line_object = self
4206 .scene_graph
4207 .objects
4208 .get(line.0)
4209 .ok_or_else(|| KclError::refactor(format!("Line not found: {line:?}")))?;
4210 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4211 let kind = line_object.kind.human_friendly_kind_with_article();
4212 return Err(KclError::refactor(format!(
4213 "This constraint only works on Segments, but you selected {kind}"
4214 )));
4215 };
4216 let Segment::Line(_) = line_segment else {
4217 return Err(KclError::refactor(format!(
4218 "Only lines can be made horizontal, but you selected {}",
4219 line_segment.human_friendly_kind_with_article(),
4220 )));
4221 };
4222 self.line_id_to_ast_reference(line, new_ast)?
4223 }
4224 Horizontal::Points { points } => {
4225 let point_asts = points
4226 .iter()
4227 .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
4228 .collect::<Result<Vec<_>, _>>()?;
4229 ast::ArrayExpression::new(point_asts).into()
4230 }
4231 };
4232 let horizontal_ast = create_horizontal_ast(first_arg_ast);
4234
4235 let (sketch_block_ref, _) = self.mutate_ast(
4237 new_ast,
4238 sketch_id,
4239 AstMutateCommand::AddSketchBlockExprStmt { expr: horizontal_ast },
4240 )?;
4241 Ok(sketch_block_ref)
4242 }
4243
4244 async fn add_lines_equal_length(
4245 &mut self,
4246 sketch: ObjectId,
4247 lines_equal_length: LinesEqualLength,
4248 new_ast: &mut ast::Node<ast::Program>,
4249 ) -> Result<AstNodeRef, KclError> {
4250 if lines_equal_length.lines.len() < 2 {
4251 return Err(KclError::refactor(format!(
4252 "Lines equal length constraint must have at least 2 lines, got {}",
4253 lines_equal_length.lines.len()
4254 )));
4255 };
4256
4257 let sketch_id = sketch;
4258
4259 let line_asts = lines_equal_length
4261 .lines
4262 .iter()
4263 .map(|line_id| {
4264 let line_object = self
4265 .scene_graph
4266 .objects
4267 .get(line_id.0)
4268 .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
4269 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4270 let kind = line_object.kind.human_friendly_kind_with_article();
4271 return Err(KclError::refactor(format!(
4272 "This constraint only works on Segments, but you selected {kind}"
4273 )));
4274 };
4275 let Segment::Line(_) = line_segment else {
4276 let kind = line_segment.human_friendly_kind_with_article();
4277 return Err(KclError::refactor(format!(
4278 "Only lines can be made equal length, but you selected {kind}"
4279 )));
4280 };
4281
4282 self.line_id_to_ast_reference(*line_id, new_ast)
4283 })
4284 .collect::<Result<Vec<_>, _>>()?;
4285
4286 let equal_length_ast = create_equal_length_ast(line_asts);
4288
4289 let (sketch_block_ref, _) = self.mutate_ast(
4291 new_ast,
4292 sketch_id,
4293 AstMutateCommand::AddSketchBlockExprStmt { expr: equal_length_ast },
4294 )?;
4295 Ok(sketch_block_ref)
4296 }
4297
4298 fn equal_radius_segment_id_to_ast_reference(
4299 &mut self,
4300 segment_id: ObjectId,
4301 new_ast: &mut ast::Node<ast::Program>,
4302 ) -> Result<ast::Expr, KclError> {
4303 let segment_object = self
4304 .scene_graph
4305 .objects
4306 .get(segment_id.0)
4307 .ok_or_else(|| KclError::refactor(format!("Segment not found: {segment_id:?}")))?;
4308 let ObjectKind::Segment { segment } = &segment_object.kind else {
4309 return Err(KclError::refactor(format!(
4310 "Object is not a segment, it was {}",
4311 segment_object.kind.human_friendly_kind_with_article()
4312 )));
4313 };
4314
4315 let ref_type = match segment {
4316 Segment::Arc(_) => ARC_VARIABLE,
4317 Segment::Circle(_) => CIRCLE_VARIABLE,
4318 _ => {
4319 return Err(KclError::refactor(format!(
4320 "equalRadius supports only arc/circle segments, got {}",
4321 segment.human_friendly_kind_with_article()
4322 )));
4323 }
4324 };
4325
4326 get_or_insert_ast_reference(new_ast, &segment_object.source, ref_type, None)
4327 }
4328
4329 fn symmetric_input_id_to_ast_reference(
4330 &mut self,
4331 segment_id: ObjectId,
4332 new_ast: &mut ast::Node<ast::Program>,
4333 ) -> Result<ast::Expr, KclError> {
4334 let segment_object = self
4335 .scene_graph
4336 .objects
4337 .get(segment_id.0)
4338 .ok_or_else(|| KclError::refactor(format!("Segment not found: {segment_id:?}")))?;
4339 let ObjectKind::Segment { segment } = &segment_object.kind else {
4340 return Err(KclError::refactor(format!(
4341 "Object is not a segment, it was {}",
4342 segment_object.kind.human_friendly_kind_with_article()
4343 )));
4344 };
4345
4346 match segment {
4347 Segment::Point(_) => self.point_id_to_ast_reference(segment_id, new_ast),
4348 Segment::Line(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, LINE_VARIABLE, None),
4349 Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, ARC_VARIABLE, None),
4350 Segment::Circle(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, CIRCLE_VARIABLE, None),
4351 Segment::ControlPointSpline(_) => Err(KclError::refactor(
4352 "Symmetric does not yet support control point splines".to_owned(),
4353 )),
4354 }
4355 }
4356
4357 fn symmetric_axis_id_to_ast_reference(
4358 &mut self,
4359 segment_id: ObjectId,
4360 new_ast: &mut ast::Node<ast::Program>,
4361 ) -> Result<ast::Expr, KclError> {
4362 let segment_object = self
4363 .scene_graph
4364 .objects
4365 .get(segment_id.0)
4366 .ok_or_else(|| KclError::refactor(format!("Axis segment not found: {segment_id:?}")))?;
4367 let ObjectKind::Segment { segment } = &segment_object.kind else {
4368 return Err(KclError::refactor(format!(
4369 "Object is not a segment, it was {}",
4370 segment_object.kind.human_friendly_kind_with_article()
4371 )));
4372 };
4373 match segment {
4374 Segment::Line(_) => self.line_id_to_ast_reference(segment_id, new_ast),
4375 _ => Err(KclError::refactor(format!(
4376 "Symmetric axis must be a line, got {}",
4377 segment.human_friendly_kind_with_article()
4378 ))),
4379 }
4380 }
4381
4382 async fn add_parallel(
4383 &mut self,
4384 sketch: ObjectId,
4385 parallel: Parallel,
4386 new_ast: &mut ast::Node<ast::Program>,
4387 ) -> Result<AstNodeRef, KclError> {
4388 if parallel.lines.len() < 2 {
4389 return Err(KclError::refactor(format!(
4390 "Parallel constraint must have at least 2 lines, got {}",
4391 parallel.lines.len()
4392 )));
4393 };
4394
4395 let sketch_id = sketch;
4396
4397 let line_asts = parallel
4398 .lines
4399 .iter()
4400 .map(|line_id| {
4401 let line_object = self
4402 .scene_graph
4403 .objects
4404 .get(line_id.0)
4405 .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
4406 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4407 let kind = line_object.kind.human_friendly_kind_with_article();
4408 return Err(KclError::refactor(format!(
4409 "This constraint only works on Segments, but you selected {kind}"
4410 )));
4411 };
4412 let Segment::Line(_) = line_segment else {
4413 let kind = line_segment.human_friendly_kind_with_article();
4414 return Err(KclError::refactor(format!(
4415 "Only lines can be made parallel, but you selected {kind}"
4416 )));
4417 };
4418
4419 self.line_id_to_ast_reference(*line_id, new_ast)
4420 })
4421 .collect::<Result<Vec<_>, _>>()?;
4422
4423 let call_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
4424 callee: ast::Node::no_src(ast_sketch2_name(LinesAtAngleKind::Parallel.to_function_name())),
4425 unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
4426 ast::ArrayExpression {
4427 elements: line_asts,
4428 digest: None,
4429 non_code_meta: Default::default(),
4430 },
4431 )))),
4432 arguments: Default::default(),
4433 digest: None,
4434 non_code_meta: Default::default(),
4435 })));
4436
4437 let (sketch_block_ref, _) = self.mutate_ast(
4438 new_ast,
4439 sketch_id,
4440 AstMutateCommand::AddSketchBlockExprStmt { expr: call_ast },
4441 )?;
4442 Ok(sketch_block_ref)
4443 }
4444
4445 async fn add_perpendicular(
4446 &mut self,
4447 sketch: ObjectId,
4448 perpendicular: Perpendicular,
4449 new_ast: &mut ast::Node<ast::Program>,
4450 ) -> Result<AstNodeRef, KclError> {
4451 self.add_lines_at_angle_constraint(sketch, LinesAtAngleKind::Perpendicular, perpendicular.lines, new_ast)
4452 .await
4453 }
4454
4455 async fn add_lines_at_angle_constraint(
4456 &mut self,
4457 sketch: ObjectId,
4458 angle_kind: LinesAtAngleKind,
4459 lines: Vec<ObjectId>,
4460 new_ast: &mut ast::Node<ast::Program>,
4461 ) -> Result<AstNodeRef, KclError> {
4462 let &[line0_id, line1_id] = lines.as_slice() else {
4463 return Err(KclError::refactor(format!(
4464 "{} constraint must have exactly 2 lines, got {}",
4465 angle_kind.to_function_name(),
4466 lines.len()
4467 )));
4468 };
4469
4470 let sketch_id = sketch;
4471
4472 let line0_object = self
4474 .scene_graph
4475 .objects
4476 .get(line0_id.0)
4477 .ok_or_else(|| KclError::refactor(format!("Line not found: {line0_id:?}")))?;
4478 let ObjectKind::Segment { segment: line0_segment } = &line0_object.kind else {
4479 let kind = line0_object.kind.human_friendly_kind_with_article();
4480 return Err(KclError::refactor(format!(
4481 "This constraint only works on Segments, but you selected {kind}"
4482 )));
4483 };
4484 let Segment::Line(_) = line0_segment else {
4485 return Err(KclError::refactor(format!(
4486 "Only lines can be made {}, but you selected {}",
4487 angle_kind.to_function_name(),
4488 line0_segment.human_friendly_kind_with_article(),
4489 )));
4490 };
4491 let line0_ast = self.line_id_to_ast_reference(line0_id, new_ast)?;
4492
4493 let line1_object = self
4494 .scene_graph
4495 .objects
4496 .get(line1_id.0)
4497 .ok_or_else(|| KclError::refactor(format!("Line not found: {line1_id:?}")))?;
4498 let ObjectKind::Segment { segment: line1_segment } = &line1_object.kind else {
4499 let kind = line1_object.kind.human_friendly_kind_with_article();
4500 return Err(KclError::refactor(format!(
4501 "This constraint only works on Segments, but you selected {kind}"
4502 )));
4503 };
4504 let Segment::Line(_) = line1_segment else {
4505 return Err(KclError::refactor(format!(
4506 "Only lines can be made {}, but you selected {}",
4507 angle_kind.to_function_name(),
4508 line1_segment.human_friendly_kind_with_article(),
4509 )));
4510 };
4511 let line1_ast = self.line_id_to_ast_reference(line1_id, new_ast)?;
4512
4513 let call_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
4515 callee: ast::Node::no_src(ast_sketch2_name(angle_kind.to_function_name())),
4516 unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
4517 ast::ArrayExpression {
4518 elements: vec![line0_ast, line1_ast],
4519 digest: None,
4520 non_code_meta: Default::default(),
4521 },
4522 )))),
4523 arguments: Default::default(),
4524 digest: None,
4525 non_code_meta: Default::default(),
4526 })));
4527
4528 let (sketch_block_ref, _) = self.mutate_ast(
4530 new_ast,
4531 sketch_id,
4532 AstMutateCommand::AddSketchBlockExprStmt { expr: call_ast },
4533 )?;
4534 Ok(sketch_block_ref)
4535 }
4536
4537 async fn add_vertical(
4538 &mut self,
4539 sketch: ObjectId,
4540 vertical: Vertical,
4541 new_ast: &mut ast::Node<ast::Program>,
4542 ) -> Result<AstNodeRef, KclError> {
4543 let sketch_id = sketch;
4544
4545 let first_arg_ast = match vertical {
4546 Vertical::Line { line } => {
4547 let line_object = self
4549 .scene_graph
4550 .objects
4551 .get(line.0)
4552 .ok_or_else(|| KclError::refactor(format!("Line not found: {line:?}")))?;
4553 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4554 let kind = line_object.kind.human_friendly_kind_with_article();
4555 return Err(KclError::refactor(format!(
4556 "This constraint only works on Segments, but you selected {kind}"
4557 )));
4558 };
4559 let Segment::Line(_) = line_segment else {
4560 return Err(KclError::refactor(format!(
4561 "Only lines can be made vertical, but you selected {}",
4562 line_segment.human_friendly_kind_with_article()
4563 )));
4564 };
4565 self.line_id_to_ast_reference(line, new_ast)?
4566 }
4567 Vertical::Points { points } => {
4568 let point_asts = points
4569 .iter()
4570 .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
4571 .collect::<Result<Vec<_>, _>>()?;
4572 ast::ArrayExpression::new(point_asts).into()
4573 }
4574 };
4575 let vertical_ast = create_vertical_ast(first_arg_ast);
4577
4578 let (sketch_block_ref, _) = self.mutate_ast(
4580 new_ast,
4581 sketch_id,
4582 AstMutateCommand::AddSketchBlockExprStmt { expr: vertical_ast },
4583 )?;
4584 Ok(sketch_block_ref)
4585 }
4586
4587 async fn execute_after_add_constraint(
4588 &mut self,
4589 ctx: &ExecutorContext,
4590 sketch_id: ObjectId,
4591 sketch_block_ref: AstNodeRef,
4592 new_ast: &mut ast::Node<ast::Program>,
4593 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
4594 let new_source = source_from_ast(new_ast);
4596 let new_program = parse_frontend_mutation_source(
4598 &new_source,
4599 "Error parsing KCL source after adding constraint",
4600 "No AST produced after adding constraint",
4601 )?;
4602 let constraint_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
4603 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
4604 "Source range of new constraint not found in sketch block: {sketch_block_ref:?}; {err:?}"
4605 )))
4606 })?;
4607
4608 let mut truncated_program = new_program.clone();
4611 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
4612 .map_err(KclErrorWithOutputs::no_outputs)?;
4613
4614 let outcome = ctx
4616 .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch_id))
4617 .await?;
4618
4619 let new_object_ids = {
4620 let constraint_id = outcome
4622 .source_range_to_object
4623 .get(&constraint_node_ref.range)
4624 .copied()
4625 .ok_or_else(|| {
4626 KclErrorWithOutputs::from_error_outcome(
4627 KclError::refactor(format!("Source range of constraint not found: {constraint_node_ref:?}")),
4628 outcome.clone(),
4629 )
4630 })?;
4631 vec![constraint_id]
4632 };
4633
4634 self.program = new_program;
4637
4638 let outcome = self.update_state_after_exec(outcome, true);
4640
4641 let src_delta = self.commit_var_solutions_to_program(&outcome, "adding constraint")?;
4642 let scene_graph_delta = SceneGraphDelta {
4643 new_graph: self.scene_graph_for_ui(),
4644 invalidates_ids: false,
4645 new_objects: new_object_ids,
4646 exec_outcome: outcome,
4647 };
4648 Ok((src_delta, scene_graph_delta))
4649 }
4650
4651 fn commit_var_solutions_to_program(&mut self, outcome: &ExecOutcome, operation: &str) -> ExecResult<SourceDelta> {
4652 let commit_failure = || {
4653 KclErrorWithOutputs::from_error_outcome(
4654 KclError::refactor(format!("Could not update KCL after {operation}.")),
4655 outcome.clone(),
4656 )
4657 };
4658
4659 let default_length_unit = self.default_length_unit();
4660 let mut settled_ast = self.program.ast.clone();
4661 let mut committed_solver_value = false;
4662 for (var_range, node_path, value) in &outcome.var_solutions {
4663 let Some(lookup) = numeric_literal_at_node_path(&settled_ast, node_path.as_ref(), *var_range) else {
4664 return Err(commit_failure());
4665 };
4666 let new_value = match &lookup {
4667 Some(current_literal) => {
4668 if !var_solution_needs_commit(current_literal, *value, default_length_unit) {
4669 continue;
4670 }
4671 preserve_var_solution_literal_style(current_literal, *value, default_length_unit)
4672 }
4673 None => {
4674 Number {
4678 value: number_value_in_default_length_units(*value, default_length_unit),
4679 units: default_length_unit.into(),
4680 }
4681 }
4682 };
4683 committed_solver_value = true;
4684 let source_ref = SourceRef::Simple {
4685 range: *var_range,
4686 node_path: node_path.clone(),
4687 };
4688 mutate_ast_node_by_source_ref(
4689 &mut settled_ast,
4690 &source_ref,
4691 AstMutateCommand::EditVarInitialValue { value: new_value },
4692 )
4693 .map_err(|_| commit_failure())?;
4694 }
4695
4696 if !committed_solver_value {
4697 return Ok(SourceDelta {
4698 text: self.program.original_file_contents.clone(),
4699 });
4700 }
4701
4702 let settled_source = source_from_ast(&settled_ast);
4703 let (settled_program, errors) = Program::parse(&settled_source).map_err(|_| commit_failure())?;
4704 if !errors.is_empty() {
4705 return Err(commit_failure());
4706 }
4707 let Some(settled_program) = settled_program else {
4708 return Err(commit_failure());
4709 };
4710
4711 self.program = settled_program;
4712
4713 Ok(SourceDelta { text: settled_source })
4714 }
4715
4716 fn segment_will_be_deleted(&self, segment_id: ObjectId, segment_ids_set: &AhashIndexSet<ObjectId>) -> bool {
4718 if segment_ids_set.contains(&segment_id) {
4719 return true;
4720 }
4721
4722 let Some(segment_object) = self.scene_graph.objects.get(segment_id.0) else {
4723 return false;
4724 };
4725 let ObjectKind::Segment { segment } = &segment_object.kind else {
4726 return false;
4727 };
4728 let Segment::Point(point) = segment else {
4729 return false;
4730 };
4731
4732 point.owner.is_some_and(|owner_id| segment_ids_set.contains(&owner_id))
4733 }
4734
4735 fn remaining_constraint_segments(
4736 &self,
4737 segments: &[ConstraintSegment],
4738 segment_ids_set: &AhashIndexSet<ObjectId>,
4739 ) -> Vec<ConstraintSegment> {
4740 segments
4741 .iter()
4742 .copied()
4743 .filter(|segment| match segment {
4744 ConstraintSegment::Origin(_) => true,
4745 ConstraintSegment::Segment(segment_id) => !self.segment_will_be_deleted(*segment_id, segment_ids_set),
4746 })
4747 .collect()
4748 }
4749
4750 fn find_referenced_constraints(
4751 &self,
4752 sketch_id: ObjectId,
4753 segment_ids_set: &AhashIndexSet<ObjectId>,
4754 ) -> Result<AhashIndexSet<ObjectId>, KclError> {
4755 let sketch_object = self
4757 .scene_graph
4758 .objects
4759 .get(sketch_id.0)
4760 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch_id:?}")))?;
4761 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
4762 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
4763 };
4764 let segment_or_owner_matches = |segment_id: ObjectId| {
4765 if segment_ids_set.contains(&segment_id) {
4766 return true;
4767 }
4768 let segment_object = self.scene_graph.objects.get(segment_id.0);
4769 if let Some(obj) = segment_object
4770 && let ObjectKind::Segment { segment } = &obj.kind
4771 {
4772 match segment {
4773 Segment::Point(point) => point.owner.is_some_and(|owner_id| segment_ids_set.contains(&owner_id)),
4774 Segment::Line(line) => line.owner.is_some_and(|owner_id| segment_ids_set.contains(&owner_id)),
4775 _ => false,
4776 }
4777 } else {
4778 false
4779 }
4780 };
4781 let mut constraint_ids_set = AhashIndexSet::default();
4782 for constraint_id in &sketch.constraints {
4783 let constraint_object = self
4784 .scene_graph
4785 .objects
4786 .get(constraint_id.0)
4787 .ok_or_else(|| KclError::refactor(format!("Constraint not found: {constraint_id:?}")))?;
4788 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
4789 return Err(KclError::refactor(format!(
4790 "Object is not a constraint, it is {}",
4791 constraint_object.kind.human_friendly_kind_with_article()
4792 )));
4793 };
4794 let depends_on_segment = match constraint {
4795 Constraint::Coincident(c) => c.segment_ids().any(segment_or_owner_matches),
4796 Constraint::Distance(d) => d.point_ids().any(segment_or_owner_matches),
4797 Constraint::Fixed(fixed) => fixed
4798 .points
4799 .iter()
4800 .any(|fixed_point| self.segment_will_be_deleted(fixed_point.point, segment_ids_set)),
4801 Constraint::Radius(r) => segment_or_owner_matches(r.arc),
4802 Constraint::Diameter(d) => segment_or_owner_matches(d.arc),
4803 Constraint::EqualRadius(equal_radius) => {
4804 equal_radius.input.iter().copied().any(segment_or_owner_matches)
4805 }
4806 Constraint::HorizontalDistance(d) => d.point_ids().any(segment_or_owner_matches),
4807 Constraint::VerticalDistance(d) => d.point_ids().any(segment_or_owner_matches),
4808 Constraint::Horizontal(h) => match h {
4809 Horizontal::Line { line } => segment_or_owner_matches(*line),
4810 Horizontal::Points { points } => points.iter().any(|point| match point {
4811 ConstraintSegment::Segment(point) => segment_or_owner_matches(*point),
4812 ConstraintSegment::Origin(_) => false,
4813 }),
4814 },
4815 Constraint::Vertical(v) => match v {
4816 Vertical::Line { line } => segment_or_owner_matches(*line),
4817 Vertical::Points { points } => points.iter().any(|point| match point {
4818 ConstraintSegment::Segment(point) => segment_or_owner_matches(*point),
4819 ConstraintSegment::Origin(_) => false,
4820 }),
4821 },
4822 Constraint::LinesEqualLength(lines_equal_length) => {
4823 lines_equal_length.lines.iter().copied().any(segment_or_owner_matches)
4824 }
4825 Constraint::Midpoint(midpoint) => {
4826 segment_or_owner_matches(midpoint.segment)
4827 || matches!(
4828 midpoint.point,
4829 ConstraintSegment::Segment(point) if segment_or_owner_matches(point)
4830 )
4831 }
4832 Constraint::Parallel(parallel) => parallel.lines.iter().copied().any(segment_or_owner_matches),
4833 Constraint::Perpendicular(perpendicular) => {
4834 perpendicular.lines.iter().copied().any(segment_or_owner_matches)
4835 }
4836 Constraint::Angle(angle) => angle.lines.iter().copied().any(segment_or_owner_matches),
4837 Constraint::Symmetric(symmetric) => {
4838 segment_or_owner_matches(symmetric.axis)
4839 || symmetric.input.iter().copied().any(segment_or_owner_matches)
4840 }
4841 Constraint::Tangent(tangent) => tangent.input.iter().copied().any(segment_or_owner_matches),
4842 };
4843 if depends_on_segment {
4844 constraint_ids_set.insert(*constraint_id);
4845 }
4846 }
4847 Ok(constraint_ids_set)
4848 }
4849
4850 fn update_state_after_exec(&mut self, outcome: ExecOutcome, freedom_analysis_ran: bool) -> ExecOutcome {
4851 let mut outcome = outcome;
4852 self.solid_references = solid_references_from_variables(&self.program.ast, &outcome.variables);
4853 let mut new_objects = std::mem::take(&mut outcome.scene_objects);
4854
4855 if freedom_analysis_ran {
4856 self.point_freedom_cache.clear();
4859 for new_obj in &new_objects {
4860 if let ObjectKind::Segment {
4861 segment: crate::front::Segment::Point(point),
4862 } = &new_obj.kind
4863 {
4864 self.point_freedom_cache.insert(new_obj.id, point.freedom);
4865 }
4866 }
4867 add_wall_and_cap_face_objects(&mut new_objects, &outcome.artifact_graph);
4868 self.scene_graph.objects = new_objects;
4870 } else {
4871 for old_obj in &self.scene_graph.objects {
4874 if let ObjectKind::Segment {
4875 segment: crate::front::Segment::Point(point),
4876 } = &old_obj.kind
4877 {
4878 self.point_freedom_cache.insert(old_obj.id, point.freedom);
4879 }
4880 }
4881
4882 let mut updated_objects = Vec::with_capacity(new_objects.len());
4884 for new_obj in new_objects {
4885 let mut obj = new_obj;
4886 if let ObjectKind::Segment {
4887 segment: crate::front::Segment::Point(point),
4888 } = &mut obj.kind
4889 {
4890 let new_freedom = point.freedom;
4891 match new_freedom {
4897 Freedom::Free => {
4898 match self.point_freedom_cache.get(&obj.id).copied() {
4899 Some(Freedom::Conflict) => {
4900 }
4903 Some(Freedom::Fixed) => {
4904 point.freedom = Freedom::Fixed;
4906 }
4907 Some(Freedom::Free) => {
4908 }
4910 None => {
4911 }
4913 }
4914 }
4915 Freedom::Fixed => {
4916 }
4918 Freedom::Conflict => {
4919 }
4921 }
4922 self.point_freedom_cache.insert(obj.id, point.freedom);
4924 }
4925 updated_objects.push(obj);
4926 }
4927
4928 add_wall_and_cap_face_objects(&mut updated_objects, &outcome.artifact_graph);
4929 self.scene_graph.objects = updated_objects;
4930 }
4931 outcome
4932 }
4933
4934 fn mutate_ast(
4935 &mut self,
4936 ast: &mut ast::Node<ast::Program>,
4937 object_id: ObjectId,
4938 command: AstMutateCommand,
4939 ) -> Result<(AstNodeRef, AstMutateCommandReturn), KclError> {
4940 let sketch_object = self
4941 .scene_graph
4942 .objects
4943 .get(object_id.0)
4944 .ok_or_else(|| KclError::refactor(format!("Object not found: {object_id:?}")))?;
4945 mutate_ast_node_by_source_ref(ast, &sketch_object.source, command)
4946 }
4947
4948 fn mutate_constraint_label_position(
4949 &mut self,
4950 ast: &mut ast::Node<ast::Program>,
4951 constraint_id: ObjectId,
4952 label_position: Point2d<Number>,
4953 ) -> Result<(), KclError> {
4954 let object = self
4955 .scene_graph
4956 .objects
4957 .get(constraint_id.0)
4958 .ok_or_else(|| KclError::refactor(format!("Object not found: {constraint_id:?}")))?;
4959 if !matches!(
4960 &object.kind,
4961 ObjectKind::Constraint {
4962 constraint: Constraint::Distance(_)
4963 | Constraint::HorizontalDistance(_)
4964 | Constraint::VerticalDistance(_)
4965 | Constraint::Radius(_)
4966 | Constraint::Diameter(_),
4967 }
4968 ) {
4969 return Err(KclError::refactor(format!(
4970 "Object does not support labelPosition: {constraint_id:?}"
4971 )));
4972 }
4973
4974 let label_position = to_ast_point2d_number(&label_position)
4975 .map_err(|err| KclError::refactor(format!("Could not convert label position to AST: {err}")))?;
4976 self.mutate_ast(
4977 ast,
4978 constraint_id,
4979 AstMutateCommand::EditDistanceConstraintLabelPosition { label_position },
4980 )?;
4981 Ok(())
4982 }
4983}
4984
4985fn sketch_block_ref_from_id(scene_graph: &SceneGraph, sketch_id: ObjectId) -> Result<AstNodeRef, KclError> {
4986 let sketch_object = scene_graph
4988 .objects
4989 .get(sketch_id.0)
4990 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch_id:?}")))?;
4991 let ObjectKind::Sketch(_) = &sketch_object.kind else {
4992 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
4993 };
4994 expect_single_node_ref(sketch_object)
4995}
4996
4997fn expect_single_node_ref(object: &Object) -> Result<AstNodeRef, KclError> {
4998 match &object.source {
4999 SourceRef::Simple { range, node_path } => Ok(AstNodeRef {
5000 range: *range,
5001 node_path: node_path.clone(),
5002 }),
5003 SourceRef::BackTrace { ranges } => {
5004 let [range] = ranges.as_slice() else {
5005 return Err(KclError::refactor(format!(
5006 "Expected single location in SourceRef, got {}; ranges={ranges:#?}",
5007 ranges.len()
5008 )));
5009 };
5010 Ok(AstNodeRef {
5011 range: range.0,
5012 node_path: range.1.clone(),
5013 })
5014 }
5015 }
5016}
5017
5018fn only_sketch_block_from_range(
5021 ast: &mut ast::Node<ast::Program>,
5022 sketch_block_range: SourceRange,
5023 edit_kind: ChangeKind,
5024) -> Result<(), KclError> {
5025 let r1 = sketch_block_range;
5026 let matches_range = |r2: SourceRange| -> bool {
5027 match edit_kind {
5030 ChangeKind::Add => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() <= r2.end(),
5031 ChangeKind::Edit => r1.module_id() == r2.module_id() && r1.start() == r2.start(),
5033 ChangeKind::Delete => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() >= r2.end(),
5034 ChangeKind::None => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() == r2.end(),
5036 }
5037 };
5038 let mut found = false;
5039 for item in ast.body.iter_mut() {
5040 match item {
5041 ast::BodyItem::ImportStatement(_) => {}
5042 ast::BodyItem::ExpressionStatement(node) => {
5043 if matches_range(SourceRange::from(&*node))
5044 && let ast::Expr::SketchBlock(sketch_block) = &mut node.expression
5045 {
5046 sketch_block.is_being_edited = true;
5047 found = true;
5048 break;
5049 }
5050 }
5051 ast::BodyItem::VariableDeclaration(node) => {
5052 if matches_range(SourceRange::from(&node.declaration.init))
5053 && let ast::Expr::SketchBlock(sketch_block) = &mut node.declaration.init
5054 {
5055 sketch_block.is_being_edited = true;
5056 found = true;
5057 break;
5058 }
5059 }
5060 ast::BodyItem::TypeDeclaration(_) => {}
5061 ast::BodyItem::ReturnStatement(node) => {
5062 if matches_range(SourceRange::from(&node.argument))
5063 && let ast::Expr::SketchBlock(sketch_block) = &mut node.argument
5064 {
5065 sketch_block.is_being_edited = true;
5066 found = true;
5067 break;
5068 }
5069 }
5070 }
5071 }
5072 if !found {
5073 return Err(KclError::refactor(format!(
5074 "Sketch block source range not found in AST: {sketch_block_range:?}, edit_kind={edit_kind:?}"
5075 )));
5076 }
5077
5078 Ok(())
5079}
5080
5081fn only_sketch_block(
5082 ast: &mut ast::Node<ast::Program>,
5083 sketch_block_ref: &AstNodeRef,
5084 edit_kind: ChangeKind,
5085) -> Result<(), KclError> {
5086 let Some(target_node_path) = &sketch_block_ref.node_path else {
5087 #[cfg(target_arch = "wasm32")]
5088 web_sys::console::warn_1(
5089 &format!(
5090 "only_sketch_block: target sketch block ref doesn't have node path; sketch_block_ref={:#?}, edit_kind={edit_kind:#?}",
5091 sketch_block_ref
5092 )
5093 .into(),
5094 );
5095 return only_sketch_block_from_range(ast, sketch_block_ref.range, edit_kind);
5096 };
5097 let mut found = false;
5098 for item in ast.body.iter_mut() {
5099 match item {
5100 ast::BodyItem::ImportStatement(_) => {}
5101 ast::BodyItem::ExpressionStatement(node) => {
5102 if let Some(node_path) = &node.node_path
5104 && node_path == target_node_path
5105 && let ast::Expr::SketchBlock(sketch_block) = &mut node.expression
5106 {
5107 sketch_block.is_being_edited = true;
5108 found = true;
5109 break;
5110 }
5111 if let Some(node_path) = node.expression.node_path()
5113 && node_path == target_node_path
5114 && let ast::Expr::SketchBlock(sketch_block) = &mut node.expression
5115 {
5116 sketch_block.is_being_edited = true;
5117 found = true;
5118 break;
5119 }
5120 }
5121 ast::BodyItem::VariableDeclaration(node) => {
5122 if let Some(node_path) = node.declaration.init.node_path()
5123 && node_path == target_node_path
5124 && let ast::Expr::SketchBlock(sketch_block) = &mut node.declaration.init
5125 {
5126 sketch_block.is_being_edited = true;
5127 found = true;
5128 break;
5129 }
5130 }
5131 ast::BodyItem::TypeDeclaration(_) => {}
5132 ast::BodyItem::ReturnStatement(node) => {
5133 if let Some(node_path) = node.argument.node_path()
5134 && node_path == target_node_path
5135 && let ast::Expr::SketchBlock(sketch_block) = &mut node.argument
5136 {
5137 sketch_block.is_being_edited = true;
5138 found = true;
5139 break;
5140 }
5141 }
5142 }
5143 }
5144 if !found {
5145 return Err(KclError::refactor(format!(
5146 "Sketch block node path not found in AST: {sketch_block_ref:?}, edit_kind={edit_kind:?}"
5147 )));
5148 }
5149
5150 Ok(())
5151}
5152
5153fn sketch_on_ast_expr(
5154 ast: &mut ast::Node<ast::Program>,
5155 scene_graph: &SceneGraph,
5156 solid_references: &HashMap<Uuid, SolidAstReference>,
5157 on: &Plane,
5158) -> Result<ast::Expr, KclError> {
5159 match on {
5160 Plane::Default(name) => Ok(default_plane_ast_expr(*name)),
5161 Plane::Object(object_id) => {
5162 let on_object = scene_graph
5163 .objects
5164 .get(object_id.0)
5165 .ok_or_else(|| KclError::refactor(format!("Sketch plane object not found: {object_id:?}")))?;
5166 if let Some(face_expr) = sketch_face_of_scene_object_ast_expr(ast, on_object)? {
5167 return Ok(face_expr);
5168 }
5169 get_or_insert_ast_reference(ast, &on_object.source, "plane", None)
5170 }
5171 Plane::PrimitiveFace(face) => {
5172 let solid_expr = solid_expr_for_engine_id(solid_references, face.solid_id).ok_or_else(|| {
5173 KclError::refactor(format!(
5174 "Could not resolve a KCL solid for selected primitive face: solid_id={}",
5175 face.solid_id
5176 ))
5177 })?;
5178 let face_id_expr = create_face_id_ast(solid_expr.clone(), face.index);
5179 Ok(create_face_of_ast(solid_expr, face_id_expr))
5180 }
5181 }
5182}
5183
5184fn solid_references_from_variables(
5185 ast: &ast::Node<ast::Program>,
5186 variables: &IndexMap<String, KclValueView>,
5187) -> HashMap<Uuid, SolidAstReference> {
5188 let mut references = HashMap::new();
5189
5190 for item in &ast.body {
5193 let ast::BodyItem::VariableDeclaration(declaration) = item else {
5194 continue;
5195 };
5196 let name = &declaration.declaration.id.name;
5197 let Some(value) = variables.get(name) else {
5198 continue;
5199 };
5200
5201 match value {
5202 KclValueView::Solid { value } => {
5203 references.insert(
5204 value.id,
5205 SolidAstReference {
5206 variable_name: name.clone(),
5207 output_index: None,
5208 },
5209 );
5210 }
5211 KclValueView::Tuple { value } | KclValueView::HomArray { value } => {
5212 for (output_index, entry) in value.iter().enumerate() {
5213 if let KclValueView::Solid { value } = entry {
5214 references.insert(
5215 value.id,
5216 SolidAstReference {
5217 variable_name: name.clone(),
5218 output_index: Some(output_index),
5219 },
5220 );
5221 }
5222 }
5223 }
5224 _ => {}
5225 }
5226 }
5227
5228 references
5229}
5230
5231fn solid_expr_for_engine_id(solid_references: &HashMap<Uuid, SolidAstReference>, solid_id: Uuid) -> Option<ast::Expr> {
5232 let reference = solid_references.get(&solid_id)?;
5233 let solid_expr = ast_name_expr(reference.variable_name.clone());
5234 Some(indexed_solid_expr_for_sweep_output(solid_expr, reference.output_index))
5235}
5236
5237fn sketch_face_of_scene_object_ast_expr(
5238 ast: &mut ast::Node<ast::Program>,
5239 on_object: &crate::front::Object,
5240) -> Result<Option<ast::Expr>, KclError> {
5241 match &on_object.kind {
5242 ObjectKind::Wall(wall) => {
5243 let solid_ref = get_or_insert_ast_reference(
5244 ast,
5245 &source_ref_from_source_ref_range(&wall.source.solid),
5246 "solid",
5247 None,
5248 )?;
5249 let ast::Expr::Name(solid_name_expr) = solid_ref else {
5250 return Err(KclError::refactor(format!(
5251 "Could not resolve solid reference for selected wall: artifact_id={:?}",
5252 on_object.artifact_id
5253 )));
5254 };
5255 let solid_expr = indexed_solid_expr_for_sweep_output(
5256 ast_name_expr(solid_name_expr.name.name.clone()),
5257 wall.solid_output_index,
5258 );
5259 let sweep_ref = get_or_insert_ast_reference(
5260 ast,
5261 &source_ref_from_source_ref_range(&wall.source.sweep),
5262 "solid",
5263 None,
5264 )?;
5265 let ast::Expr::Name(sweep_name_expr) = sweep_ref else {
5266 return Err(KclError::refactor(format!(
5267 "Could not resolve sweep reference for selected wall: artifact_id={:?}",
5268 on_object.artifact_id
5269 )));
5270 };
5271 let sweep_name = sweep_name_expr.name.name.clone();
5272 let segment_ref = get_or_insert_ast_reference(
5273 ast,
5274 &source_ref_from_source_ref_range(&wall.source.segment),
5275 LINE_VARIABLE,
5276 None,
5277 )?;
5278
5279 let face_expr = if let Some(region_name) = region_name_from_sweep_variable(ast, &sweep_name).or_else(|| {
5280 wall.source
5281 .path
5282 .as_ref()
5283 .and_then(|path_source| region_name_from_path_source(ast, path_source))
5284 }) {
5285 let ast::Expr::Name(segment_name_expr) = segment_ref else {
5286 return Err(KclError::refactor(format!(
5287 "Could not resolve source segment reference for selected region wall: artifact_id={:?}",
5288 on_object.artifact_id
5289 )));
5290 };
5291 create_member_expression(
5292 create_member_expression(ast_name_expr(region_name), "tags"),
5293 &segment_name_expr.name.name,
5294 )
5295 } else {
5296 segment_ref
5297 };
5298
5299 Ok(Some(create_face_of_ast(solid_expr, face_expr)))
5300 }
5301 ObjectKind::Cap(cap) => {
5302 let solid_ref =
5303 get_or_insert_ast_reference(ast, &source_ref_from_source_ref_range(&cap.source.solid), "solid", None)?;
5304 let ast::Expr::Name(solid_name_expr) = solid_ref else {
5305 return Err(KclError::refactor(format!(
5306 "Could not resolve solid reference for selected cap: artifact_id={:?}",
5307 on_object.artifact_id
5308 )));
5309 };
5310 let solid_expr = indexed_solid_expr_for_sweep_output(
5311 ast_name_expr(solid_name_expr.name.name.clone()),
5312 cap.solid_output_index,
5313 );
5314 let face_expr = match cap.kind {
5316 crate::frontend::api::CapKind::Start => ast_name_expr("START".to_owned()),
5317 crate::frontend::api::CapKind::End => ast_name_expr("END".to_owned()),
5318 };
5319
5320 Ok(Some(create_face_of_ast(solid_expr, face_expr)))
5321 }
5322 _ => Ok(None),
5323 }
5324}
5325
5326fn indexed_solid_expr_for_sweep_output(solid_expr: ast::Expr, solid_output_index: Option<usize>) -> ast::Expr {
5327 match solid_output_index {
5328 Some(output_index) => create_index_expression(solid_expr, output_index),
5329 None => solid_expr,
5330 }
5331}
5332
5333fn source_ref_from_source_ref_range(source: &SourceRefRange) -> SourceRef {
5334 SourceRef::Simple {
5335 range: source.range,
5336 node_path: source.node_path.clone(),
5337 }
5338}
5339
5340fn region_name_from_path_source(ast: &ast::Node<ast::Program>, path_source: &SourceRefRange) -> Option<String> {
5341 let source_ref = source_ref_from_source_ref_range(path_source);
5342 let candidate = variable_name_containing_source_ref(ast, &source_ref)?;
5343 let ast::Definition::Variable(region_decl) = ast.get_variable(&candidate)? else {
5344 return None;
5345 };
5346 let ast::Expr::CallExpressionKw(region_call) = ®ion_decl.init else {
5347 return None;
5348 };
5349 if region_call.callee.name.name != "region" {
5350 return None;
5351 }
5352 Some(candidate)
5353}
5354
5355fn downstream_composite_code_ref_for_source(artifact_graph: &ArtifactGraph, source_id: ArtifactId) -> Option<&CodeRef> {
5356 let mut current_id = source_id;
5357 let mut current_composite = None;
5358 let mut visited = HashSet::new();
5359
5360 while visited.insert(current_id) {
5361 let next_composite_id = downstream_composite_id_for_solid_source(artifact_graph, current_id);
5362
5363 let Some(composite_id) = next_composite_id else {
5364 break;
5365 };
5366 let Some(Artifact::CompositeSolid(composite)) = artifact_graph.get(&composite_id) else {
5367 break;
5368 };
5369
5370 current_id = composite.id;
5371 current_composite = Some(composite);
5372
5373 if !composite.consumed {
5374 break;
5375 }
5376 }
5377
5378 current_composite.map(|composite| &composite.code_ref)
5379}
5380
5381fn downstream_composite_id_for_solid_source(
5382 artifact_graph: &ArtifactGraph,
5383 source_id: ArtifactId,
5384) -> Option<ArtifactId> {
5385 if let Some(Artifact::Path(path)) = artifact_graph.get(&source_id)
5387 && let Some(composite_id) = path.composite_solid_id
5388 && let Some(Artifact::CompositeSolid(composite)) = artifact_graph.get(&composite_id)
5389 && composite_contains_path_input(&composite.solid_ids, &composite.tool_ids, path.id, path.solid2d_id)
5390 {
5391 return Some(composite_id);
5392 }
5393
5394 for artifact in artifact_graph.values() {
5396 if let Artifact::Path(path) = artifact
5397 && path.sweep_id == Some(source_id)
5398 && let Some(composite_id) = path.composite_solid_id
5399 && let Some(Artifact::CompositeSolid(composite)) = artifact_graph.get(&composite_id)
5400 && composite_contains_path_input(&composite.solid_ids, &composite.tool_ids, path.id, path.solid2d_id)
5401 {
5402 return Some(composite_id);
5403 }
5404 }
5405
5406 artifact_graph.values().find_map(|artifact| {
5408 let Artifact::CompositeSolid(composite) = artifact else {
5409 return None;
5410 };
5411 composite_contains_input(&composite.solid_ids, &composite.tool_ids, source_id).then_some(composite.id)
5412 })
5413}
5414
5415fn composite_contains_path_input(
5416 solid_ids: &[ArtifactId],
5417 tool_ids: &[ArtifactId],
5418 path_id: ArtifactId,
5419 solid2d_id: Option<ArtifactId>,
5420) -> bool {
5421 composite_contains_input(solid_ids, tool_ids, path_id)
5422 || solid2d_id.is_some_and(|solid2d_id| composite_contains_input(solid_ids, tool_ids, solid2d_id))
5423}
5424
5425fn composite_contains_input(solid_ids: &[ArtifactId], tool_ids: &[ArtifactId], input_id: ArtifactId) -> bool {
5426 solid_ids.contains(&input_id) || tool_ids.contains(&input_id)
5427}
5428
5429fn code_ref_source_ref_range(code_ref: &CodeRef) -> SourceRefRange {
5430 let node_path = (!code_ref.node_path.is_empty()).then(|| code_ref.node_path.clone());
5431 SourceRefRange {
5432 range: code_ref.range,
5433 node_path,
5434 }
5435}
5436
5437fn solid_output_index_for_sweep(
5438 artifact_graph: &ArtifactGraph,
5439 sweep_id: ArtifactId,
5440 sweep_code_ref: &CodeRef,
5441) -> Option<usize> {
5442 let sibling_sweeps = artifact_graph
5443 .values()
5444 .filter_map(|artifact| match artifact {
5445 Artifact::Sweep(sweep)
5446 if sweep.code_ref.range == sweep_code_ref.range
5447 && sweep.code_ref.node_path == sweep_code_ref.node_path =>
5448 {
5449 Some(sweep)
5450 }
5451 _ => None,
5452 })
5453 .collect::<Vec<_>>();
5454
5455 if sibling_sweeps.len() <= 1 {
5456 return None;
5457 }
5458
5459 sibling_sweeps
5460 .iter()
5461 .position(|sibling_sweep| sibling_sweep.id == sweep_id)
5462}
5463
5464fn add_wall_and_cap_face_objects(scene_objects: &mut Vec<crate::front::Object>, artifact_graph: &ArtifactGraph) {
5465 let mut existing_artifact_ids = scene_objects
5466 .iter()
5467 .map(|object| object.artifact_id)
5468 .collect::<HashSet<_>>();
5469
5470 for artifact in artifact_graph.values() {
5471 match artifact {
5472 Artifact::Wall(wall) => {
5473 if existing_artifact_ids.contains(&wall.id) {
5474 continue;
5475 }
5476
5477 let Some(segment) = artifact_graph.get(&wall.seg_id).and_then(|artifact| match artifact {
5478 Artifact::Segment(segment) => Some(segment),
5479 _ => None,
5480 }) else {
5481 continue;
5482 };
5483 let Some(sweep) = artifact_graph.get(&wall.sweep_id).and_then(|artifact| match artifact {
5484 Artifact::Sweep(sweep) => Some(sweep),
5485 _ => None,
5486 }) else {
5487 continue;
5488 };
5489 let source_segment = segment
5490 .original_seg_id
5491 .and_then(|original_seg_id| artifact_graph.get(&original_seg_id))
5492 .and_then(|artifact| match artifact {
5493 Artifact::Segment(segment) => Some(segment),
5494 _ => None,
5495 })
5496 .unwrap_or(segment);
5497 let solid_code_ref =
5498 downstream_composite_code_ref_for_source(artifact_graph, wall.sweep_id).unwrap_or(&sweep.code_ref);
5499 let path_code_ref = artifact_graph
5500 .get(&segment.path_id)
5501 .or_else(|| artifact_graph.get(&sweep.path_id))
5502 .and_then(|artifact| match artifact {
5503 Artifact::Path(path) => Some(&path.code_ref),
5504 _ => None,
5505 });
5506 let source = WallSource {
5507 solid: code_ref_source_ref_range(solid_code_ref),
5508 sweep: code_ref_source_ref_range(&sweep.code_ref),
5509 path: path_code_ref.map(code_ref_source_ref_range),
5510 segment: code_ref_source_ref_range(&source_segment.code_ref),
5511 };
5512 let solid_output_index = (solid_code_ref.range == sweep.code_ref.range
5513 && solid_code_ref.node_path == sweep.code_ref.node_path)
5514 .then(|| solid_output_index_for_sweep(artifact_graph, sweep.id, &sweep.code_ref))
5515 .flatten();
5516 let object_source = source_ref_from_source_ref_range(&source.solid);
5517 let id = ObjectId(scene_objects.len());
5518 scene_objects.push(crate::front::Object {
5519 id,
5520 kind: ObjectKind::Wall(crate::frontend::api::Wall {
5521 id,
5522 source,
5523 solid_output_index,
5524 }),
5525 label: Default::default(),
5526 comments: Default::default(),
5527 artifact_id: wall.id,
5528 source: object_source,
5529 });
5530 existing_artifact_ids.insert(wall.id);
5531 }
5532 Artifact::Cap(cap) => {
5533 if existing_artifact_ids.contains(&cap.id) {
5534 continue;
5535 }
5536
5537 let Some(sweep) = artifact_graph.get(&cap.sweep_id).and_then(|artifact| match artifact {
5538 Artifact::Sweep(sweep) => Some(sweep),
5539 _ => None,
5540 }) else {
5541 continue;
5542 };
5543 let id = ObjectId(scene_objects.len());
5544 let kind = match cap.sub_type {
5545 CapSubType::Start => crate::frontend::api::CapKind::Start,
5546 CapSubType::End => crate::frontend::api::CapKind::End,
5547 };
5548 let solid_code_ref =
5549 downstream_composite_code_ref_for_source(artifact_graph, cap.sweep_id).unwrap_or(&sweep.code_ref);
5550 let source = CapSource {
5551 solid: code_ref_source_ref_range(solid_code_ref),
5552 sweep: code_ref_source_ref_range(&sweep.code_ref),
5553 };
5554 let solid_output_index = (solid_code_ref.range == sweep.code_ref.range
5555 && solid_code_ref.node_path == sweep.code_ref.node_path)
5556 .then(|| solid_output_index_for_sweep(artifact_graph, sweep.id, &sweep.code_ref))
5557 .flatten();
5558 let object_source = source_ref_from_source_ref_range(&source.solid);
5559 scene_objects.push(crate::front::Object {
5560 id,
5561 kind: ObjectKind::Cap(crate::frontend::api::Cap {
5562 id,
5563 kind,
5564 source,
5565 solid_output_index,
5566 }),
5567 label: Default::default(),
5568 comments: Default::default(),
5569 artifact_id: cap.id,
5570 source: object_source,
5571 });
5572 existing_artifact_ids.insert(cap.id);
5573 }
5574 _ => {}
5575 }
5576 }
5577}
5578
5579fn default_plane_ast_expr(name: crate::engine::PlaneName) -> ast::Expr {
5580 use crate::engine::PlaneName;
5581
5582 match name {
5583 PlaneName::Xy => ast_name_expr("XY".to_owned()),
5584 PlaneName::Xz => ast_name_expr("XZ".to_owned()),
5585 PlaneName::Yz => ast_name_expr("YZ".to_owned()),
5586 PlaneName::NegXy => negated_plane_ast_expr("XY"),
5587 PlaneName::NegXz => negated_plane_ast_expr("XZ"),
5588 PlaneName::NegYz => negated_plane_ast_expr("YZ"),
5589 }
5590}
5591
5592fn negated_plane_ast_expr(name: &str) -> ast::Expr {
5593 ast::Expr::UnaryExpression(Box::new(ast::UnaryExpression::new(
5594 ast::UnaryOperator::Neg,
5595 ast::BinaryPart::Name(Box::new(ast_name(name.to_owned()))),
5596 )))
5597}
5598
5599fn create_face_of_ast(solid_expr: ast::Expr, face_expr: ast::Expr) -> ast::Expr {
5600 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5601 callee: ast::Node::no_src(ast_sketch2_name("faceOf")),
5602 unlabeled: Some(solid_expr),
5603 arguments: vec![ast::LabeledArg {
5604 label: Some(ast::Identifier::new("face")),
5605 arg: face_expr,
5606 }],
5607 digest: None,
5608 non_code_meta: Default::default(),
5609 })))
5610}
5611
5612fn create_face_id_ast(solid_expr: ast::Expr, index: usize) -> ast::Expr {
5613 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5614 callee: ast::Node::no_src(ast_sketch2_name("faceId")),
5615 unlabeled: Some(solid_expr),
5616 arguments: vec![ast::LabeledArg {
5617 label: Some(ast::Identifier::new("index")),
5618 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(ast::NumericLiteral {
5619 value: index as f64,
5620 suffix: NumericSuffix::None,
5621 raw: index.to_string(),
5622 digest: None,
5623 })))),
5624 }],
5625 digest: None,
5626 non_code_meta: Default::default(),
5627 })))
5628}
5629
5630fn region_name_from_sweep_variable(ast: &ast::Node<ast::Program>, sweep_variable_name: &str) -> Option<String> {
5631 let ast::Definition::Variable(sweep_decl) = ast.get_variable(sweep_variable_name)? else {
5632 return None;
5633 };
5634 let ast::Expr::CallExpressionKw(sweep_call) = &sweep_decl.init else {
5635 return None;
5636 };
5637 if !matches!(
5638 sweep_call.callee.name.name.as_str(),
5639 "extrude" | "revolve" | "sweep" | "loft"
5640 ) {
5641 return None;
5642 }
5643 let ast::Expr::Name(region_name_expr) = sweep_call.unlabeled.as_ref()? else {
5644 return None;
5645 };
5646 let candidate = region_name_expr.name.name.clone();
5647 let ast::Definition::Variable(region_decl) = ast.get_variable(&candidate)? else {
5648 return None;
5649 };
5650 let ast::Expr::CallExpressionKw(region_call) = ®ion_decl.init else {
5651 return None;
5652 };
5653 if region_call.callee.name.name != "region" {
5654 return None;
5655 }
5656 Some(candidate)
5657}
5658
5659fn get_or_insert_ast_reference(
5666 ast: &mut ast::Node<ast::Program>,
5667 source_ref: &SourceRef,
5668 prefix: &str,
5669 property: Option<&str>,
5670) -> Result<ast::Expr, KclError> {
5671 let command = AstMutateCommand::AddVariableDeclaration {
5672 prefix: prefix.to_owned(),
5673 };
5674 let ret = match mutate_ast_node_by_source_ref(ast, source_ref, command) {
5675 Ok((_, ret)) => ret,
5676 Err(err) => {
5677 if let Some(var_name) = variable_name_containing_source_ref(ast, source_ref) {
5678 AstMutateCommandReturn::Name(var_name)
5679 } else {
5680 return Err(err);
5681 }
5682 }
5683 };
5684 let AstMutateCommandReturn::Name(var_name) = ret else {
5685 return Err(KclError::refactor(
5686 "Expected variable name returned from AddVariableDeclaration".to_owned(),
5687 ));
5688 };
5689 let var_expr = ast::Expr::Name(Box::new(ast::Name::new(&var_name)));
5690 let Some(property) = property else {
5691 return Ok(var_expr);
5693 };
5694
5695 Ok(create_member_expression(var_expr, property))
5696}
5697
5698fn variable_name_containing_source_ref(ast: &ast::Node<ast::Program>, source_ref: &SourceRef) -> Option<String> {
5699 let source_range = match source_ref {
5700 SourceRef::Simple { range, .. } => *range,
5701 SourceRef::BackTrace { ranges } => {
5702 let [range] = ranges.as_slice() else {
5703 return None;
5704 };
5705 range.0
5706 }
5707 };
5708 ast.body.iter().find_map(|item| {
5709 let ast::BodyItem::VariableDeclaration(var_decl) = item else {
5710 return None;
5711 };
5712 let init_range = SourceRange::from(&var_decl.declaration.init);
5713 let source_is_inside_init = init_range.module_id() == source_range.module_id()
5714 && init_range.start() <= source_range.start()
5715 && source_range.end() <= init_range.end();
5716 if matches!(&var_decl.declaration.init, ast::Expr::SketchBlock(_))
5717 && init_range != source_range
5718 && source_is_inside_init
5719 {
5720 return None;
5721 }
5722 source_is_inside_init.then(|| var_decl.name().to_owned())
5723 })
5724}
5725
5726fn mutate_ast_node_by_source_ref(
5727 ast: &mut ast::Node<ast::Program>,
5728 source_ref: &SourceRef,
5729 command: AstMutateCommand,
5730) -> Result<(AstNodeRef, AstMutateCommandReturn), KclError> {
5731 let (source_range, node_path) = match source_ref {
5732 SourceRef::Simple { range, node_path } => (*range, node_path.clone()),
5733 SourceRef::BackTrace { ranges } => {
5734 let [range] = ranges.as_slice() else {
5735 return Err(KclError::refactor(format!(
5736 "Expected single source ref, got {}; ranges={ranges:#?}",
5737 ranges.len(),
5738 )));
5739 };
5740 (range.0, range.1.clone())
5741 }
5742 };
5743 let mut context = AstMutateContext {
5744 source_range,
5745 node_path,
5746 command,
5747 defined_names_stack: Default::default(),
5748 };
5749 let control = dfs_mut(ast, &mut context);
5750 match control {
5751 ControlFlow::Continue(_) => Err(KclError::refactor(
5752 "Could not find the KCL source for this edit. Try reloading the app, or update from code.".to_owned(),
5753 )),
5754 ControlFlow::Break(break_value) => break_value,
5755 }
5756}
5757
5758#[derive(Debug)]
5759struct AstMutateContext {
5760 source_range: SourceRange,
5761 node_path: Option<ast::NodePath>,
5762 command: AstMutateCommand,
5763 defined_names_stack: Vec<HashSet<String>>,
5764}
5765
5766#[derive(Debug)]
5767#[allow(clippy::large_enum_variant)]
5768enum AstMutateCommand {
5769 AddSketchBlockExprStmt {
5771 expr: ast::Expr,
5772 },
5773 AddSketchBlockVarDecl {
5775 prefix: String,
5776 expr: ast::Expr,
5777 },
5778 AddVariableDeclaration {
5779 prefix: String,
5780 },
5781 EditPoint {
5782 at: ast::Expr,
5783 },
5784 EditLine {
5785 start: ast::Expr,
5786 end: ast::Expr,
5787 construction: Option<bool>,
5788 },
5789 EditArc {
5790 start: ast::Expr,
5791 end: ast::Expr,
5792 center: ast::Expr,
5793 construction: Option<bool>,
5794 },
5795 EditCircle {
5796 start: ast::Expr,
5797 center: ast::Expr,
5798 construction: Option<bool>,
5799 },
5800 EditControlPointSpline {
5801 points: ast::Expr,
5802 construction: Option<bool>,
5803 },
5804 EditConstraintValue {
5805 value: ast::BinaryPart,
5806 },
5807 EditDistanceConstraintLabelPosition {
5808 label_position: ast::Expr,
5809 },
5810 EditCallUnlabeled {
5811 arg: ast::Expr,
5812 },
5813 EditVarInitialValue {
5814 value: Number,
5815 },
5816 DeleteNode,
5817}
5818
5819impl AstMutateCommand {
5820 fn needs_defined_names_stack(&self) -> bool {
5821 matches!(
5822 self,
5823 AstMutateCommand::AddSketchBlockVarDecl { .. } | AstMutateCommand::AddVariableDeclaration { .. }
5824 )
5825 }
5826}
5827
5828#[derive(Debug)]
5829enum AstMutateCommandReturn {
5830 None,
5831 Name(String),
5832}
5833
5834#[derive(Debug, Clone)]
5835struct AstNodeRef {
5836 range: SourceRange,
5837 node_path: Option<ast::NodePath>,
5838}
5839
5840impl<T> From<&ast::Node<T>> for AstNodeRef {
5841 fn from(value: &ast::Node<T>) -> Self {
5842 AstNodeRef {
5843 range: value.into(),
5844 node_path: value.node_path.clone(),
5845 }
5846 }
5847}
5848
5849impl From<&ast::BodyItem> for AstNodeRef {
5850 fn from(value: &ast::BodyItem) -> Self {
5851 match value {
5852 ast::BodyItem::ImportStatement(node) => AstNodeRef {
5853 range: node.into(),
5854 node_path: node.node_path.clone(),
5855 },
5856 ast::BodyItem::ExpressionStatement(node) => AstNodeRef {
5857 range: node.into(),
5858 node_path: node.node_path.clone(),
5859 },
5860 ast::BodyItem::VariableDeclaration(node) => AstNodeRef {
5861 range: node.into(),
5862 node_path: node.node_path.clone(),
5863 },
5864 ast::BodyItem::TypeDeclaration(node) => AstNodeRef {
5865 range: node.into(),
5866 node_path: node.node_path.clone(),
5867 },
5868 ast::BodyItem::ReturnStatement(node) => AstNodeRef {
5869 range: node.into(),
5870 node_path: node.node_path.clone(),
5871 },
5872 }
5873 }
5874}
5875
5876impl From<&ast::Expr> for AstNodeRef {
5877 fn from(value: &ast::Expr) -> Self {
5878 AstNodeRef {
5879 range: SourceRange::from(value),
5880 node_path: value.node_path().cloned(),
5881 }
5882 }
5883}
5884
5885impl From<&AstMutateContext> for AstNodeRef {
5886 fn from(value: &AstMutateContext) -> Self {
5887 AstNodeRef {
5888 range: value.source_range,
5889 node_path: value.node_path.clone(),
5890 }
5891 }
5892}
5893
5894impl TryFrom<&NodeMut<'_>> for AstNodeRef {
5895 type Error = crate::walk::AstNodeError;
5896
5897 fn try_from(value: &NodeMut<'_>) -> Result<Self, Self::Error> {
5898 Ok(AstNodeRef {
5899 range: SourceRange::try_from(value)?,
5900 node_path: value.try_into()?,
5901 })
5902 }
5903}
5904
5905impl From<AstNodeRef> for SourceRange {
5906 fn from(value: AstNodeRef) -> Self {
5907 value.range
5908 }
5909}
5910
5911impl Visitor for AstMutateContext {
5912 type Break = Result<(AstNodeRef, AstMutateCommandReturn), KclError>;
5913 type Continue = ();
5914
5915 fn visit(&mut self, node: NodeMut<'_>) -> TraversalReturn<Self::Break, Self::Continue> {
5916 filter_and_process(self, node)
5917 }
5918
5919 fn finish(&mut self, node: NodeMut<'_>) {
5920 match &node {
5921 NodeMut::Program(_) | NodeMut::SketchBlock(_) => {
5922 self.defined_names_stack.pop();
5923 }
5924 _ => {}
5925 }
5926 }
5927}
5928
5929fn filter_and_process(
5930 ctx: &mut AstMutateContext,
5931 node: NodeMut,
5932) -> TraversalReturn<Result<(AstNodeRef, AstMutateCommandReturn), KclError>> {
5933 let Ok(node_range) = SourceRange::try_from(&node) else {
5934 return TraversalReturn::new_continue(());
5936 };
5937 if let NodeMut::VariableDeclaration(var_decl) = &node {
5942 let expr_range = SourceRange::from(&var_decl.declaration.init);
5943 let expr_node_path = var_decl.declaration.init.node_path();
5944 if source_ref_matches(ctx, expr_range, expr_node_path) {
5945 if let AstMutateCommand::AddVariableDeclaration { .. } = &ctx.command {
5946 return TraversalReturn::new_break(Ok((
5949 AstNodeRef::from(&**var_decl),
5950 AstMutateCommandReturn::Name(var_decl.name().to_owned()),
5951 )));
5952 }
5953 if let AstMutateCommand::DeleteNode = &ctx.command {
5954 return TraversalReturn {
5957 mutate_body_item: MutateBodyItem::Delete,
5958 control_flow: ControlFlow::Break(Ok((AstNodeRef::from(&*ctx), AstMutateCommandReturn::None))),
5959 };
5960 }
5961 }
5962 }
5963 if let NodeMut::ExpressionStatement(expr_stmt) = &node {
5966 let expr_range = SourceRange::from(&expr_stmt.expression);
5967 let expr_node_path = expr_stmt.expression.node_path();
5968 if source_ref_matches(ctx, expr_range, expr_node_path) {
5969 if let AstMutateCommand::AddVariableDeclaration { .. } = &ctx.command {
5970 let Ok(node_ref) = AstNodeRef::try_from(&node) else {
5973 return TraversalReturn::new_continue(());
5974 };
5975 return process(ctx, node).map_break(|result| result.map(|cmd_return| (node_ref, cmd_return)));
5976 }
5977 if let AstMutateCommand::DeleteNode = &ctx.command {
5978 return TraversalReturn {
5981 mutate_body_item: MutateBodyItem::Delete,
5982 control_flow: ControlFlow::Break(Ok((AstNodeRef::from(&*ctx), AstMutateCommandReturn::None))),
5983 };
5984 }
5985 }
5986 }
5987
5988 if ctx.command.needs_defined_names_stack() {
5989 if let NodeMut::Program(program) = &node {
5990 ctx.defined_names_stack.push(find_defined_names(*program));
5991 } else if let NodeMut::SketchBlock(block) = &node {
5992 ctx.defined_names_stack.push(find_defined_names(&block.body));
5993 }
5994 }
5995
5996 let node_path = <Option<ast::NodePath>>::try_from(&node).ok().flatten();
5998 if !source_ref_matches(ctx, node_range, node_path.as_ref()) {
5999 return TraversalReturn::new_continue(());
6000 }
6001 let Ok(node_ref) = AstNodeRef::try_from(&node) else {
6002 return TraversalReturn::new_continue(());
6003 };
6004 process(ctx, node).map_break(|result| result.map(|cmd_return| (node_ref, cmd_return)))
6005}
6006
6007fn source_ref_matches(ctx: &AstMutateContext, node_range: SourceRange, node_path: Option<&ast::NodePath>) -> bool {
6008 match &ctx.node_path {
6009 Some(target) => Some(target) == node_path,
6010 None => node_range == ctx.source_range,
6011 }
6012}
6013
6014fn process(ctx: &AstMutateContext, node: NodeMut) -> TraversalReturn<Result<AstMutateCommandReturn, KclError>> {
6015 match &ctx.command {
6016 AstMutateCommand::AddSketchBlockExprStmt { expr } => {
6017 if let NodeMut::SketchBlock(sketch_block) = node {
6018 sketch_block
6019 .body
6020 .items
6021 .push(ast::BodyItem::ExpressionStatement(ast::Node {
6022 inner: ast::ExpressionStatement {
6023 expression: expr.clone(),
6024 digest: None,
6025 },
6026 start: Default::default(),
6027 end: Default::default(),
6028 module_id: Default::default(),
6029 node_path: None,
6030 outer_attrs: Default::default(),
6031 pre_comments: Default::default(),
6032 comment_start: Default::default(),
6033 }));
6034 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6035 }
6036 }
6037 AstMutateCommand::AddSketchBlockVarDecl { prefix, expr } => {
6038 if let NodeMut::SketchBlock(sketch_block) = node {
6039 let empty_defined_names = HashSet::new();
6040 let defined_names = ctx.defined_names_stack.last().unwrap_or(&empty_defined_names);
6041 let Ok(name) = next_free_name(prefix, defined_names) else {
6042 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6043 };
6044 sketch_block
6045 .body
6046 .items
6047 .push(ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(
6048 ast::VariableDeclaration::new(
6049 ast::VariableDeclarator::new(&name, expr.clone()),
6050 ast::ItemVisibility::Default,
6051 ast::VariableKind::Const,
6052 ),
6053 ))));
6054 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::Name(name)));
6055 }
6056 }
6057 AstMutateCommand::AddVariableDeclaration { prefix } => {
6058 if let NodeMut::VariableDeclaration(inner) = node {
6059 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::Name(inner.name().to_owned())));
6060 }
6061 if let NodeMut::ExpressionStatement(expr_stmt) = node {
6062 let empty_defined_names = HashSet::new();
6063 let defined_names = ctx.defined_names_stack.last().unwrap_or(&empty_defined_names);
6064 let Ok(name) = next_free_name(prefix, defined_names) else {
6065 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6067 };
6068 let mutate_node =
6069 ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(ast::VariableDeclaration::new(
6070 ast::VariableDeclarator::new(&name, expr_stmt.expression.clone()),
6071 ast::ItemVisibility::Default,
6072 ast::VariableKind::Const,
6073 ))));
6074 return TraversalReturn {
6075 mutate_body_item: MutateBodyItem::Mutate(Box::new(mutate_node)),
6076 control_flow: ControlFlow::Break(Ok(AstMutateCommandReturn::Name(name))),
6077 };
6078 }
6079 }
6080 AstMutateCommand::EditPoint { at } => {
6081 if let NodeMut::CallExpressionKw(call) = node {
6082 if call.callee.name.name != POINT_FN {
6083 return TraversalReturn::new_continue(());
6084 }
6085 for labeled_arg in &mut call.arguments {
6087 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(POINT_AT_PARAM) {
6088 labeled_arg.arg = at.clone();
6089 }
6090 }
6091 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6092 }
6093 }
6094 AstMutateCommand::EditLine {
6095 start,
6096 end,
6097 construction,
6098 } => {
6099 if let NodeMut::CallExpressionKw(call) = node {
6100 if call.callee.name.name != LINE_FN {
6101 return TraversalReturn::new_continue(());
6102 }
6103 for labeled_arg in &mut call.arguments {
6105 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(LINE_START_PARAM) {
6106 labeled_arg.arg = start.clone();
6107 }
6108 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(LINE_END_PARAM) {
6109 labeled_arg.arg = end.clone();
6110 }
6111 }
6112 if let Some(construction_value) = construction {
6114 let construction_exists = call
6115 .arguments
6116 .iter()
6117 .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
6118 if *construction_value {
6119 if construction_exists {
6121 for labeled_arg in &mut call.arguments {
6123 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
6124 labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
6125 value: ast::LiteralValue::Bool(true),
6126 raw: "true".to_string(),
6127 digest: None,
6128 })));
6129 }
6130 }
6131 } else {
6132 call.arguments.push(ast::LabeledArg {
6134 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
6135 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
6136 value: ast::LiteralValue::Bool(true),
6137 raw: "true".to_string(),
6138 digest: None,
6139 }))),
6140 });
6141 }
6142 } else {
6143 call.arguments
6145 .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
6146 }
6147 }
6148 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6149 }
6150 }
6151 AstMutateCommand::EditArc {
6152 start,
6153 end,
6154 center,
6155 construction,
6156 } => {
6157 if let NodeMut::CallExpressionKw(call) = node {
6158 if call.callee.name.name != ARC_FN {
6159 return TraversalReturn::new_continue(());
6160 }
6161 for labeled_arg in &mut call.arguments {
6163 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_START_PARAM) {
6164 labeled_arg.arg = start.clone();
6165 }
6166 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_END_PARAM) {
6167 labeled_arg.arg = end.clone();
6168 }
6169 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_CENTER_PARAM) {
6170 labeled_arg.arg = center.clone();
6171 }
6172 }
6173 if let Some(construction_value) = construction {
6175 let construction_exists = call
6176 .arguments
6177 .iter()
6178 .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
6179 if *construction_value {
6180 if construction_exists {
6182 for labeled_arg in &mut call.arguments {
6184 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
6185 labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
6186 value: ast::LiteralValue::Bool(true),
6187 raw: "true".to_string(),
6188 digest: None,
6189 })));
6190 }
6191 }
6192 } else {
6193 call.arguments.push(ast::LabeledArg {
6195 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
6196 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
6197 value: ast::LiteralValue::Bool(true),
6198 raw: "true".to_string(),
6199 digest: None,
6200 }))),
6201 });
6202 }
6203 } else {
6204 call.arguments
6206 .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
6207 }
6208 }
6209 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6210 }
6211 }
6212 AstMutateCommand::EditCircle {
6213 start,
6214 center,
6215 construction,
6216 } => {
6217 if let NodeMut::CallExpressionKw(call) = node {
6218 if call.callee.name.name != CIRCLE_FN {
6219 return TraversalReturn::new_continue(());
6220 }
6221 for labeled_arg in &mut call.arguments {
6223 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CIRCLE_START_PARAM) {
6224 labeled_arg.arg = start.clone();
6225 }
6226 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CIRCLE_CENTER_PARAM) {
6227 labeled_arg.arg = center.clone();
6228 }
6229 }
6230 if let Some(construction_value) = construction {
6232 let construction_exists = call
6233 .arguments
6234 .iter()
6235 .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
6236 if *construction_value {
6237 if construction_exists {
6238 for labeled_arg in &mut call.arguments {
6240 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
6241 labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
6242 value: ast::LiteralValue::Bool(true),
6243 raw: "true".to_string(),
6244 digest: None,
6245 })));
6246 }
6247 }
6248 } else {
6249 call.arguments.push(ast::LabeledArg {
6251 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
6252 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
6253 value: ast::LiteralValue::Bool(true),
6254 raw: "true".to_string(),
6255 digest: None,
6256 }))),
6257 });
6258 }
6259 } else {
6260 call.arguments
6262 .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
6263 }
6264 }
6265 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6266 }
6267 }
6268 AstMutateCommand::EditControlPointSpline { points, construction } => {
6269 if let NodeMut::CallExpressionKw(call) = node {
6270 if call.callee.name.name != CONTROL_POINT_SPLINE_FN {
6271 return TraversalReturn::new_continue(());
6272 }
6273 for labeled_arg in &mut call.arguments {
6274 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONTROL_POINT_SPLINE_POINTS_PARAM)
6275 {
6276 labeled_arg.arg = points.clone();
6277 }
6278 }
6279 if let Some(construction_value) = construction {
6281 let construction_exists = call
6282 .arguments
6283 .iter()
6284 .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
6285 if *construction_value {
6286 if construction_exists {
6287 for labeled_arg in &mut call.arguments {
6288 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
6289 labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
6290 value: ast::LiteralValue::Bool(true),
6291 raw: "true".to_string(),
6292 digest: None,
6293 })));
6294 }
6295 }
6296 } else {
6297 call.arguments.push(ast::LabeledArg {
6298 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
6299 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
6300 value: ast::LiteralValue::Bool(true),
6301 raw: "true".to_string(),
6302 digest: None,
6303 }))),
6304 });
6305 }
6306 } else {
6307 call.arguments
6308 .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
6309 }
6310 }
6311 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6312 }
6313 }
6314 AstMutateCommand::EditConstraintValue { value } => {
6315 if let NodeMut::BinaryExpression(binary_expr) = node {
6316 let left_is_constraint = matches!(
6317 &binary_expr.left,
6318 ast::BinaryPart::CallExpressionKw(call)
6319 if matches!(
6320 call.callee.name.name.as_str(),
6321 DISTANCE_FN | HORIZONTAL_DISTANCE_FN | VERTICAL_DISTANCE_FN | RADIUS_FN | DIAMETER_FN | ANGLE_FN
6322 )
6323 );
6324 if left_is_constraint {
6325 binary_expr.right = value.clone();
6326 } else {
6327 binary_expr.left = value.clone();
6328 }
6329
6330 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6331 }
6332 }
6333 AstMutateCommand::EditDistanceConstraintLabelPosition { label_position } => {
6334 if let NodeMut::BinaryExpression(binary_expr) = node {
6335 let ast::BinaryPart::CallExpressionKw(call) = &mut binary_expr.left else {
6336 return TraversalReturn::new_continue(());
6337 };
6338 if !matches!(
6339 call.callee.name.name.as_str(),
6340 DISTANCE_FN | HORIZONTAL_DISTANCE_FN | VERTICAL_DISTANCE_FN | RADIUS_FN | DIAMETER_FN
6341 ) {
6342 return TraversalReturn::new_continue(());
6343 }
6344
6345 if let Some(label_arg) = call
6346 .arguments
6347 .iter_mut()
6348 .find(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(LABEL_POSITION_PARAM))
6349 {
6350 label_arg.arg = label_position.clone();
6351 } else {
6352 call.arguments.push(ast::LabeledArg {
6353 label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
6354 arg: label_position.clone(),
6355 });
6356 }
6357
6358 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6359 }
6360 }
6361 AstMutateCommand::EditCallUnlabeled { arg } => {
6362 if let NodeMut::CallExpressionKw(call) = node {
6363 call.unlabeled = Some(arg.clone());
6364 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6365 }
6366 }
6367 AstMutateCommand::EditVarInitialValue { value } => {
6368 if let NodeMut::SketchVar(sketch_var) = node {
6372 let Ok(literal) = to_source_number(*value) else {
6373 return TraversalReturn::new_break(Err(KclError::refactor(format!(
6374 "Could not convert number to AST literal: {:?}",
6375 *value
6376 ))));
6377 };
6378 sketch_var.initial = Some(Box::new(ast::Node::no_src(literal)));
6379 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6380 }
6381 }
6382 AstMutateCommand::DeleteNode => {
6383 return TraversalReturn {
6384 mutate_body_item: MutateBodyItem::Delete,
6385 control_flow: ControlFlow::Break(Ok(AstMutateCommandReturn::None)),
6386 };
6387 }
6388 }
6389 TraversalReturn::new_continue(())
6390}
6391
6392struct FindSketchBlockSourceRange {
6393 target_before_mutation: SourceRange,
6395 found: Cell<Option<AstNodeRef>>,
6399}
6400
6401impl<'a> crate::walk::Visitor<'a> for &FindSketchBlockSourceRange {
6402 type Error = crate::front::Error;
6403
6404 fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
6405 let Ok(node_range) = SourceRange::try_from(&node) else {
6406 return Ok(true);
6407 };
6408
6409 if let crate::walk::Node::SketchBlock(sketch_block) = node {
6410 if node_range.module_id() == self.target_before_mutation.module_id()
6411 && node_range.start() == self.target_before_mutation.start()
6412 && node_range.end() >= self.target_before_mutation.end()
6414 {
6415 self.found.set(sketch_block.body.items.last().map(|item| match item {
6416 ast::BodyItem::VariableDeclaration(node) => AstNodeRef::from(&node.declaration.init),
6420 _ => AstNodeRef::from(item),
6421 }));
6422 return Ok(false);
6423 } else {
6424 return Ok(true);
6427 }
6428 }
6429
6430 for child in node.children().iter() {
6431 if !child.visit(*self)? {
6432 return Ok(false);
6433 }
6434 }
6435
6436 Ok(true)
6437 }
6438}
6439
6440struct FindSketchBlockByNodePath {
6441 target_node_path: ast::NodePath,
6443 found: Cell<Option<AstNodeRef>>,
6447}
6448
6449impl<'a> crate::walk::Visitor<'a> for &FindSketchBlockByNodePath {
6450 type Error = crate::front::Error;
6451
6452 fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
6453 let Ok(node_path) = <Option<ast::NodePath>>::try_from(&node) else {
6454 return Ok(true);
6455 };
6456
6457 if let crate::walk::Node::SketchBlock(sketch_block) = node {
6458 if let Some(node_path) = node_path
6459 && node_path == self.target_node_path
6460 {
6461 self.found.set(sketch_block.body.items.last().map(|item| match item {
6462 ast::BodyItem::VariableDeclaration(node) => AstNodeRef::from(&node.declaration.init),
6466 _ => AstNodeRef::from(item),
6467 }));
6468
6469 return Ok(false);
6470 } else {
6471 return Ok(true);
6474 }
6475 }
6476
6477 for child in node.children().iter() {
6478 if !child.visit(*self)? {
6479 return Ok(false);
6480 }
6481 }
6482
6483 Ok(true)
6484 }
6485}
6486
6487fn find_sketch_block_added_item(
6495 ast: &ast::Node<ast::Program>,
6496 sketch_block_before_mutation: &AstNodeRef,
6497) -> Result<AstNodeRef, KclError> {
6498 if let Some(node_path) = &sketch_block_before_mutation.node_path {
6499 let find = FindSketchBlockByNodePath {
6500 target_node_path: node_path.clone(),
6501 found: Cell::new(None),
6502 };
6503 let node = crate::walk::Node::from(ast);
6504 node.visit(&find).map_err(|err| KclError::refactor(err.msg))?;
6505 find.found.into_inner().ok_or_else(|| {
6506 KclError::refactor(format!(
6507 "Node ID after mutation not found for Node ID before mutation: {node_path:?}"
6508 ))
6509 })
6510 } else {
6511 let find = FindSketchBlockSourceRange {
6513 target_before_mutation: sketch_block_before_mutation.range,
6514 found: Cell::new(None),
6515 };
6516 let node = crate::walk::Node::from(ast);
6517 node.visit(&find).map_err(|err| KclError::refactor(err.msg))?;
6518 find.found.into_inner().ok_or_else(|| KclError::refactor(
6519 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?"),
6520 ))
6521 }
6522}
6523
6524fn format_kcl_error_message(prefix: &str, error: &KclError) -> String {
6525 let message = error.message().trim();
6526 let message = if message.is_empty() {
6527 "unknown parse error"
6528 } else {
6529 message
6530 };
6531
6532 format!("{prefix}: {message}")
6533}
6534
6535fn parse_frontend_mutation_source(source: &str, parse_error_prefix: &str, no_ast_message: &str) -> ExecResult<Program> {
6536 let (program, errors) = Program::parse(source).map_err(|err| {
6537 KclErrorWithOutputs::no_outputs(KclError::refactor(format_kcl_error_message(parse_error_prefix, &err)))
6538 })?;
6539 if !errors.is_empty() {
6540 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
6541 format_compilation_issues(parse_error_prefix, &errors),
6542 )));
6543 }
6544
6545 program.ok_or_else(|| KclErrorWithOutputs::no_outputs(KclError::refactor(no_ast_message.to_owned())))
6546}
6547
6548fn format_compilation_issues(prefix: &str, issues: &[CompilationIssue]) -> String {
6549 let Some(first_issue) = issues
6550 .iter()
6551 .find(|issue| issue.severity.is_err())
6552 .or_else(|| issues.first())
6553 else {
6554 return prefix.to_owned();
6555 };
6556
6557 let message = first_issue.message.trim();
6558 let message = if message.is_empty() {
6559 "unknown parse error"
6560 } else {
6561 message
6562 };
6563
6564 if issues.len() > 1 {
6565 format!("{prefix}: {message} (+{} more)", issues.len() - 1)
6566 } else {
6567 format!("{prefix}: {message}")
6568 }
6569}
6570
6571fn source_from_ast(ast: &ast::Node<ast::Program>) -> String {
6572 ast.recast_top(&Default::default(), 0)
6574}
6575
6576struct FindNumericLiteral {
6577 target: SourceRange,
6578 found: Cell<Option<ast::NumericLiteral>>,
6579}
6580
6581impl<'a> crate::walk::Visitor<'a> for &FindNumericLiteral {
6582 type Error = crate::front::Error;
6583
6584 fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
6585 let Ok(node_range) = SourceRange::try_from(&node) else {
6586 return Ok(true);
6587 };
6588
6589 if node_range == self.target
6590 && let crate::walk::Node::NumericLiteral(literal) = node
6591 {
6592 self.found.set(Some(literal.inner.clone()));
6593 return Ok(false);
6594 }
6595
6596 for child in node.children().iter() {
6597 if !child.visit(*self)? {
6598 return Ok(false);
6599 }
6600 }
6601
6602 Ok(true)
6603 }
6604}
6605
6606fn numeric_literal_at_source_range(ast: &ast::Node<ast::Program>, target: SourceRange) -> Option<ast::NumericLiteral> {
6607 let find = FindNumericLiteral {
6608 target,
6609 found: Cell::new(None),
6610 };
6611 let node = crate::walk::Node::from(ast);
6612 node.visit(&find).ok()?;
6613 find.found.into_inner()
6614}
6615
6616struct FindSketchVarInitialByNodePath<'a> {
6617 target: &'a ast::NodePath,
6618 sketch_var_found: Cell<bool>,
6619 initial_literal: Cell<Option<ast::NumericLiteral>>,
6620}
6621
6622impl<'a, 'b> crate::walk::Visitor<'b> for &FindSketchVarInitialByNodePath<'a> {
6623 type Error = crate::front::Error;
6624
6625 fn visit_node(&self, node: crate::walk::Node<'b>) -> anyhow::Result<bool, Self::Error> {
6626 if let crate::walk::Node::SketchVar(sketch_var) = node
6627 && sketch_var.node_path.as_ref() == Some(self.target)
6628 {
6629 self.sketch_var_found.set(true);
6630 if let Some(initial) = &sketch_var.initial {
6631 self.initial_literal.set(Some(initial.inner.clone()));
6632 }
6633 return Ok(false);
6634 }
6635
6636 for child in node.children().iter() {
6637 if !child.visit(*self)? {
6638 return Ok(false);
6639 }
6640 }
6641
6642 Ok(true)
6643 }
6644}
6645
6646fn numeric_literal_at_node_path(
6656 ast: &ast::Node<ast::Program>,
6657 node_path: Option<&ast::NodePath>,
6658 source_range: SourceRange,
6659) -> Option<Option<ast::NumericLiteral>> {
6660 let Some(node_path) = node_path else {
6661 let message = "numeric_literal_at_node_path: missing node_path on var solution; falling back to source-range lookup, which can fail under whitespace shifts";
6662 #[cfg(target_arch = "wasm32")]
6663 web_sys::console::warn_1(&message.into());
6664 #[cfg(not(target_arch = "wasm32"))]
6665 eprintln!("WARNING: {message}");
6666 return numeric_literal_at_source_range(ast, source_range).map(Some);
6667 };
6668 let find = FindSketchVarInitialByNodePath {
6669 target: node_path,
6670 sketch_var_found: Cell::new(false),
6671 initial_literal: Cell::new(None),
6672 };
6673 let node = crate::walk::Node::from(ast);
6674 node.visit(&find).ok()?;
6675 if !find.sketch_var_found.get() {
6676 return None;
6677 }
6678 Some(find.initial_literal.into_inner())
6679}
6680
6681fn suffix_length_unit(suffix: NumericSuffix) -> Option<UnitLength> {
6682 match suffix {
6683 NumericSuffix::Mm => Some(UnitLength::Millimeters),
6684 NumericSuffix::Cm => Some(UnitLength::Centimeters),
6685 NumericSuffix::M => Some(UnitLength::Meters),
6686 NumericSuffix::Inch => Some(UnitLength::Inches),
6687 NumericSuffix::Ft => Some(UnitLength::Feet),
6688 NumericSuffix::Yd => Some(UnitLength::Yards),
6689 _ => None,
6690 }
6691}
6692
6693fn number_value_in_default_length_units(number: Number, default_length_unit: UnitLength) -> f64 {
6694 match suffix_length_unit(number.units) {
6695 Some(unit) => adjust_length(unit, number.value, default_length_unit).0,
6696 None => number.value,
6697 }
6698}
6699
6700fn literal_value_in_default_length_units(literal: &ast::NumericLiteral, default_length_unit: UnitLength) -> f64 {
6701 match suffix_length_unit(literal.suffix) {
6702 Some(unit) => adjust_length(unit, literal.value, default_length_unit).0,
6703 None => literal.value,
6704 }
6705}
6706
6707fn var_solution_needs_commit(
6708 current_literal: &ast::NumericLiteral,
6709 solved_value: Number,
6710 default_length_unit: UnitLength,
6711) -> bool {
6712 let current = literal_value_in_default_length_units(current_literal, default_length_unit);
6713 let solved = number_value_in_default_length_units(solved_value, default_length_unit);
6714
6715 (current - solved).abs() > 1e-9
6716}
6717
6718fn preserve_var_solution_literal_style(
6719 current_literal: &ast::NumericLiteral,
6720 solved_value: Number,
6721 default_length_unit: UnitLength,
6722) -> Number {
6723 if current_literal.suffix == NumericSuffix::None {
6724 return Number {
6725 value: number_value_in_default_length_units(solved_value, default_length_unit),
6726 units: NumericSuffix::None,
6727 };
6728 }
6729
6730 let Some(current_unit) = suffix_length_unit(current_literal.suffix) else {
6731 return solved_value;
6732 };
6733
6734 let solved_default_value = number_value_in_default_length_units(solved_value, default_length_unit);
6735 Number {
6736 value: adjust_length(default_length_unit, solved_default_value, current_unit).0,
6737 units: current_literal.suffix,
6738 }
6739}
6740
6741pub(crate) fn to_ast_point2d(point: &Point2d<Expr>) -> anyhow::Result<ast::Expr> {
6742 Ok(ast::Expr::ArrayExpression(Box::new(ast::Node {
6743 inner: ast::ArrayExpression {
6744 elements: vec![to_source_expr(&point.x)?, to_source_expr(&point.y)?],
6745 non_code_meta: Default::default(),
6746 digest: None,
6747 },
6748 start: Default::default(),
6749 end: Default::default(),
6750 module_id: Default::default(),
6751 node_path: None,
6752 outer_attrs: Default::default(),
6753 pre_comments: Default::default(),
6754 comment_start: Default::default(),
6755 })))
6756}
6757
6758pub(crate) fn to_ast_point2d_array(points: &[Point2d<Expr>]) -> anyhow::Result<ast::Expr> {
6759 Ok(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
6760 ast::ArrayExpression {
6761 elements: points.iter().map(to_ast_point2d).collect::<anyhow::Result<Vec<_>>>()?,
6762 digest: None,
6763 non_code_meta: Default::default(),
6764 },
6765 ))))
6766}
6767
6768fn to_ast_point2d_number(point: &Point2d<Number>) -> anyhow::Result<ast::Expr> {
6769 Ok(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
6770 ast::ArrayExpression {
6771 elements: vec![
6772 ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(to_source_number(
6773 point.x,
6774 )?)))),
6775 ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(to_source_number(
6776 point.y,
6777 )?)))),
6778 ],
6779 non_code_meta: Default::default(),
6780 digest: None,
6781 },
6782 ))))
6783}
6784
6785fn to_source_expr(expr: &Expr) -> anyhow::Result<ast::Expr> {
6786 match expr {
6787 Expr::Number(number) => Ok(ast::Expr::Literal(Box::new(ast::Node {
6788 inner: ast::Literal::from(to_source_number(*number)?),
6789 start: Default::default(),
6790 end: Default::default(),
6791 module_id: Default::default(),
6792 node_path: None,
6793 outer_attrs: Default::default(),
6794 pre_comments: Default::default(),
6795 comment_start: Default::default(),
6796 }))),
6797 Expr::Var(number) => Ok(ast::Expr::SketchVar(Box::new(ast::Node {
6798 inner: ast::SketchVar {
6799 initial: Some(Box::new(ast::Node {
6800 inner: to_source_number(*number)?,
6801 start: Default::default(),
6802 end: Default::default(),
6803 module_id: Default::default(),
6804 node_path: None,
6805 outer_attrs: Default::default(),
6806 pre_comments: Default::default(),
6807 comment_start: Default::default(),
6808 })),
6809 digest: None,
6810 },
6811 start: Default::default(),
6812 end: Default::default(),
6813 module_id: Default::default(),
6814 node_path: None,
6815 outer_attrs: Default::default(),
6816 pre_comments: Default::default(),
6817 comment_start: Default::default(),
6818 }))),
6819 Expr::Variable(variable) => Ok(ast_name_expr(variable.clone())),
6820 }
6821}
6822
6823fn to_source_number(number: Number) -> anyhow::Result<ast::NumericLiteral> {
6824 Ok(ast::NumericLiteral {
6825 value: number.value,
6826 suffix: number.units,
6827 raw: format_number_literal(number.value, number.units, None)?,
6828 digest: None,
6829 })
6830}
6831
6832pub(crate) fn ast_name_expr(name: String) -> ast::Expr {
6833 ast::Expr::Name(Box::new(ast_name(name)))
6834}
6835
6836fn ast_name(name: String) -> ast::Node<ast::Name> {
6837 ast::Node {
6838 inner: ast::Name {
6839 name: ast::Node {
6840 inner: ast::Identifier { name, digest: None },
6841 start: Default::default(),
6842 end: Default::default(),
6843 module_id: Default::default(),
6844 node_path: None,
6845 outer_attrs: Default::default(),
6846 pre_comments: Default::default(),
6847 comment_start: Default::default(),
6848 },
6849 path: Vec::new(),
6850 abs_path: false,
6851 digest: None,
6852 },
6853 start: Default::default(),
6854 end: Default::default(),
6855 module_id: Default::default(),
6856 node_path: None,
6857 outer_attrs: Default::default(),
6858 pre_comments: Default::default(),
6859 comment_start: Default::default(),
6860 }
6861}
6862
6863pub(crate) fn ast_sketch2_name(name: &str) -> ast::Name {
6864 ast::Name {
6865 name: ast::Node {
6866 inner: ast::Identifier {
6867 name: name.to_owned(),
6868 digest: None,
6869 },
6870 start: Default::default(),
6871 end: Default::default(),
6872 module_id: Default::default(),
6873 node_path: None,
6874 outer_attrs: Default::default(),
6875 pre_comments: Default::default(),
6876 comment_start: Default::default(),
6877 },
6878 path: Default::default(),
6879 abs_path: false,
6880 digest: None,
6881 }
6882}
6883
6884pub(crate) fn create_coincident_ast(exprs: impl IntoIterator<Item = ast::Expr>) -> ast::Expr {
6888 let elements = exprs.into_iter().collect::<Vec<_>>();
6889 debug_assert!(elements.len() >= 2, "Coincident AST should have at least 2 inputs");
6890
6891 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
6893 elements,
6894 digest: None,
6895 non_code_meta: Default::default(),
6896 })));
6897
6898 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6900 callee: ast::Node::no_src(ast_sketch2_name(COINCIDENT_FN)),
6901 unlabeled: Some(array_expr),
6902 arguments: Default::default(),
6903 digest: None,
6904 non_code_meta: Default::default(),
6905 })))
6906}
6907
6908pub(crate) fn create_line_ast(start_ast: ast::Expr, end_ast: ast::Expr) -> ast::Expr {
6910 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6911 callee: ast::Node::no_src(ast_sketch2_name(LINE_FN)),
6912 unlabeled: None,
6913 arguments: vec![
6914 ast::LabeledArg {
6915 label: Some(ast::Identifier::new(LINE_START_PARAM)),
6916 arg: start_ast,
6917 },
6918 ast::LabeledArg {
6919 label: Some(ast::Identifier::new(LINE_END_PARAM)),
6920 arg: end_ast,
6921 },
6922 ],
6923 digest: None,
6924 non_code_meta: Default::default(),
6925 })))
6926}
6927
6928pub(crate) fn create_arc_ast(start_ast: ast::Expr, end_ast: ast::Expr, center_ast: ast::Expr) -> ast::Expr {
6930 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6931 callee: ast::Node::no_src(ast_sketch2_name(ARC_FN)),
6932 unlabeled: None,
6933 arguments: vec![
6934 ast::LabeledArg {
6935 label: Some(ast::Identifier::new(ARC_START_PARAM)),
6936 arg: start_ast,
6937 },
6938 ast::LabeledArg {
6939 label: Some(ast::Identifier::new(ARC_END_PARAM)),
6940 arg: end_ast,
6941 },
6942 ast::LabeledArg {
6943 label: Some(ast::Identifier::new(ARC_CENTER_PARAM)),
6944 arg: center_ast,
6945 },
6946 ],
6947 digest: None,
6948 non_code_meta: Default::default(),
6949 })))
6950}
6951
6952pub(crate) fn create_circle_ast(start_ast: ast::Expr, center_ast: ast::Expr) -> ast::Expr {
6954 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6955 callee: ast::Node::no_src(ast_sketch2_name(CIRCLE_FN)),
6956 unlabeled: None,
6957 arguments: vec![
6958 ast::LabeledArg {
6959 label: Some(ast::Identifier::new(CIRCLE_START_PARAM)),
6960 arg: start_ast,
6961 },
6962 ast::LabeledArg {
6963 label: Some(ast::Identifier::new(CIRCLE_CENTER_PARAM)),
6964 arg: center_ast,
6965 },
6966 ],
6967 digest: None,
6968 non_code_meta: Default::default(),
6969 })))
6970}
6971
6972pub(crate) fn create_horizontal_ast(line_expr: ast::Expr) -> ast::Expr {
6974 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6975 callee: ast::Node::no_src(ast_sketch2_name(HORIZONTAL_FN)),
6976 unlabeled: Some(line_expr),
6977 arguments: Default::default(),
6978 digest: None,
6979 non_code_meta: Default::default(),
6980 })))
6981}
6982
6983pub(crate) fn create_vertical_ast(line_expr: ast::Expr) -> ast::Expr {
6985 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6986 callee: ast::Node::no_src(ast_sketch2_name(VERTICAL_FN)),
6987 unlabeled: Some(line_expr),
6988 arguments: Default::default(),
6989 digest: None,
6990 non_code_meta: Default::default(),
6991 })))
6992}
6993
6994pub(crate) fn create_member_expression(object_expr: ast::Expr, property: &str) -> ast::Expr {
6996 ast::Expr::MemberExpression(Box::new(ast::Node::no_src(ast::MemberExpression {
6997 object: object_expr,
6998 property: ast::Expr::Name(Box::new(ast::Node::no_src(ast::Name {
6999 name: ast::Node::no_src(ast::Identifier {
7000 name: property.to_string(),
7001 digest: None,
7002 }),
7003 path: Vec::new(),
7004 abs_path: false,
7005 digest: None,
7006 }))),
7007 computed: false,
7008 digest: None,
7009 })))
7010}
7011
7012pub(crate) fn create_index_expression(object_expr: ast::Expr, index: usize) -> ast::Expr {
7013 ast::Expr::MemberExpression(Box::new(ast::Node::no_src(ast::MemberExpression {
7014 object: object_expr,
7015 property: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(ast::NumericLiteral {
7016 value: index as f64,
7017 suffix: NumericSuffix::None,
7018 raw: index.to_string(),
7019 digest: None,
7020 })))),
7021 computed: true,
7022 digest: None,
7023 })))
7024}
7025
7026fn create_fixed_point_constraint_ast(point_expr: ast::Expr, position: Point2d<Number>) -> anyhow::Result<ast::Expr> {
7028 let x_literal = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(to_source_number(
7030 position.x,
7031 )?))));
7032 let y_literal = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(to_source_number(
7033 position.y,
7034 )?))));
7035 let point_array = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
7036 elements: vec![x_literal, y_literal],
7037 digest: None,
7038 non_code_meta: Default::default(),
7039 })));
7040
7041 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
7043 elements: vec![point_expr, point_array],
7044 digest: None,
7045 non_code_meta: Default::default(),
7046 })));
7047
7048 Ok(ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(
7050 ast::CallExpressionKw {
7051 callee: ast::Node::no_src(ast_sketch2_name(FIXED_FN)),
7052 unlabeled: Some(array_expr),
7053 arguments: Default::default(),
7054 digest: None,
7055 non_code_meta: Default::default(),
7056 },
7057 ))))
7058}
7059
7060pub(crate) fn create_equal_length_ast(line_exprs: Vec<ast::Expr>) -> ast::Expr {
7062 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
7063 elements: line_exprs,
7064 digest: None,
7065 non_code_meta: Default::default(),
7066 })));
7067
7068 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
7070 callee: ast::Node::no_src(ast_sketch2_name(EQUAL_LENGTH_FN)),
7071 unlabeled: Some(array_expr),
7072 arguments: Default::default(),
7073 digest: None,
7074 non_code_meta: Default::default(),
7075 })))
7076}
7077
7078pub(crate) fn create_equal_radius_ast(segment_exprs: Vec<ast::Expr>) -> ast::Expr {
7080 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
7081 elements: segment_exprs,
7082 digest: None,
7083 non_code_meta: Default::default(),
7084 })));
7085
7086 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
7087 callee: ast::Node::no_src(ast_sketch2_name(EQUAL_RADIUS_FN)),
7088 unlabeled: Some(array_expr),
7089 arguments: Default::default(),
7090 digest: None,
7091 non_code_meta: Default::default(),
7092 })))
7093}
7094
7095pub(crate) fn create_tangent_ast(seg1_expr: ast::Expr, seg2_expr: ast::Expr) -> ast::Expr {
7097 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
7098 elements: vec![seg1_expr, seg2_expr],
7099 digest: None,
7100 non_code_meta: Default::default(),
7101 })));
7102
7103 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
7104 callee: ast::Node::no_src(ast_sketch2_name(TANGENT_FN)),
7105 unlabeled: Some(array_expr),
7106 arguments: Default::default(),
7107 digest: None,
7108 non_code_meta: Default::default(),
7109 })))
7110}
7111
7112pub(crate) fn create_symmetric_ast(input_exprs: Vec<ast::Expr>, axis_expr: ast::Expr) -> ast::Expr {
7114 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
7115 elements: input_exprs,
7116 digest: None,
7117 non_code_meta: Default::default(),
7118 })));
7119 let arguments = vec![ast::LabeledArg {
7120 label: Some(ast::Identifier::new(SYMMETRIC_AXIS_PARAM)),
7121 arg: axis_expr,
7122 }];
7123
7124 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
7125 callee: ast::Node::no_src(ast_sketch2_name(SYMMETRIC_FN)),
7126 unlabeled: Some(array_expr),
7127 arguments,
7128 digest: None,
7129 non_code_meta: Default::default(),
7130 })))
7131}
7132
7133pub(crate) fn create_midpoint_ast(segment_expr: ast::Expr, point_expr: ast::Expr) -> ast::Expr {
7135 let arguments = vec![ast::LabeledArg {
7136 label: Some(ast::Identifier::new(MIDPOINT_POINT_PARAM)),
7137 arg: point_expr,
7138 }];
7139
7140 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
7141 callee: ast::Node::no_src(ast_sketch2_name(MIDPOINT_FN)),
7142 unlabeled: Some(segment_expr),
7143 arguments,
7144 digest: None,
7145 non_code_meta: Default::default(),
7146 })))
7147}
7148
7149#[cfg(test)]
7150mod tests {
7151 use std::sync;
7152
7153 use super::*;
7154 use crate::engine::PlaneName;
7155 use crate::engine::engine_manager::EngineManager;
7156 use crate::execution::cache::SketchModeState;
7157 use crate::execution::cache::clear_mem_cache;
7158 use crate::execution::cache::read_old_memory;
7159 use crate::execution::cache::write_old_memory;
7160 use crate::front::Distance;
7161 use crate::front::Fixed;
7162 use crate::front::FixedPoint;
7163 use crate::front::Midpoint;
7164 use crate::front::Object;
7165 use crate::front::Plane;
7166 use crate::front::Sketch;
7167 use crate::front::Tangent;
7168 use crate::frontend::sketch::Vertical;
7169 use crate::pretty::NumericSuffix;
7170
7171 fn find_first_sketch_object(scene_graph: &SceneGraph) -> Option<&Object> {
7172 for object in &scene_graph.objects {
7173 if let ObjectKind::Sketch(_) = &object.kind {
7174 return Some(object);
7175 }
7176 }
7177 None
7178 }
7179
7180 fn find_first_face_object(scene_graph: &SceneGraph) -> Option<&Object> {
7181 for object in &scene_graph.objects {
7182 if let ObjectKind::Face(_) = &object.kind {
7183 return Some(object);
7184 }
7185 }
7186 None
7187 }
7188
7189 fn find_first_wall_object_id(scene_graph: &SceneGraph) -> Option<ObjectId> {
7190 for object in &scene_graph.objects {
7191 if matches!(&object.kind, ObjectKind::Wall(_)) {
7192 return Some(object.id);
7193 }
7194 }
7195 None
7196 }
7197
7198 fn find_cap_object_id_with_solid_output_index(
7199 scene_graph: &SceneGraph,
7200 cap_kind: crate::frontend::api::CapKind,
7201 solid_output_index: usize,
7202 ) -> Option<ObjectId> {
7203 for object in &scene_graph.objects {
7204 if matches!(&object.kind, ObjectKind::Cap(cap) if cap.kind == cap_kind && cap.solid_output_index == Some(solid_output_index))
7205 {
7206 return Some(object.id);
7207 }
7208 }
7209 None
7210 }
7211
7212 #[test]
7213 fn test_region_name_from_sweep_variable_supports_sweep_kinds() {
7214 let source = "\
7215region001 = region(point = [0.1, 0.1], sketch = s)
7216extrude001 = extrude(region001, length = 5)
7217revolve001 = revolve(region001, axis = Y)
7218sweep001 = sweep(region001, path = path001)
7219loft001 = loft(region001)
7220not_sweep001 = shell(extrude001, faces = [], thickness = 1)
7221";
7222
7223 let program = Program::parse(source).unwrap().0.unwrap();
7224
7225 assert_eq!(
7226 region_name_from_sweep_variable(&program.ast, "extrude001"),
7227 Some("region001".to_owned())
7228 );
7229 assert_eq!(
7230 region_name_from_sweep_variable(&program.ast, "revolve001"),
7231 Some("region001".to_owned())
7232 );
7233 assert_eq!(
7234 region_name_from_sweep_variable(&program.ast, "sweep001"),
7235 Some("region001".to_owned())
7236 );
7237 assert_eq!(
7238 region_name_from_sweep_variable(&program.ast, "loft001"),
7239 Some("region001".to_owned())
7240 );
7241 assert_eq!(region_name_from_sweep_variable(&program.ast, "not_sweep001"), None);
7242 }
7243
7244 #[track_caller]
7245 fn expect_sketch(object: &Object) -> &Sketch {
7246 if let ObjectKind::Sketch(sketch) = &object.kind {
7247 sketch
7248 } else {
7249 panic!("Object is not a sketch: {:?}", object);
7250 }
7251 }
7252
7253 fn point_position(scene_graph: &SceneGraph, point_id: ObjectId) -> Point2d<Number> {
7254 let point_object = scene_graph.objects.get(point_id.0).unwrap();
7255 let ObjectKind::Segment {
7256 segment: Segment::Point(point),
7257 } = &point_object.kind
7258 else {
7259 panic!("Object is not a point segment: {point_object:?}");
7260 };
7261 point.position.clone()
7262 }
7263
7264 fn assert_point_position_close(actual: Point2d<Number>, expected: Point2d<Number>) {
7265 assert!((actual.x.value - expected.x.value).abs() < 1e-6);
7266 assert!((actual.y.value - expected.y.value).abs() < 1e-6);
7267 }
7268
7269 fn point_expr_mm(x: f64, y: f64) -> Point2d<Expr> {
7272 Point2d {
7273 x: Expr::Var(Number {
7274 value: x,
7275 units: NumericSuffix::Mm,
7276 }),
7277 y: Expr::Var(Number {
7278 value: y,
7279 units: NumericSuffix::Mm,
7280 }),
7281 }
7282 }
7283
7284 fn point_number_mm(x: f64, y: f64) -> Point2d<Number> {
7287 Point2d {
7288 x: Number {
7289 value: x,
7290 units: NumericSuffix::Mm,
7291 },
7292 y: Number {
7293 value: y,
7294 units: NumericSuffix::Mm,
7295 },
7296 }
7297 }
7298
7299 fn make_line_ctor(start_x: f64, start_y: f64, end_x: f64, end_y: f64, units: NumericSuffix) -> LineCtor {
7300 LineCtor {
7301 start: Point2d {
7302 x: Expr::Number(Number { value: start_x, units }),
7303 y: Expr::Number(Number { value: start_y, units }),
7304 },
7305 end: Point2d {
7306 x: Expr::Number(Number { value: end_x, units }),
7307 y: Expr::Number(Number { value: end_y, units }),
7308 },
7309 construction: None,
7310 }
7311 }
7312
7313 async fn create_sketch_with_single_line(
7314 frontend: &mut FrontendState,
7315 ctx: &ExecutorContext,
7316 mock_ctx: &ExecutorContext,
7317 version: Version,
7318 ) -> (ObjectId, ObjectId, SourceDelta, SceneGraphDelta) {
7319 frontend.program = Program::empty();
7320
7321 let sketch_args = SketchCtor {
7322 on: Plane::Default(PlaneName::Xy),
7323 };
7324 let (_src_delta, _scene_delta, sketch_id) = frontend
7325 .new_sketch(ctx, ProjectId(0), FileId(0), version, sketch_args)
7326 .await
7327 .unwrap();
7328
7329 let segment = SegmentCtor::Line(make_line_ctor(0.0, 0.0, 10.0, 10.0, NumericSuffix::Mm));
7330 let (source_delta, scene_graph_delta) = frontend
7331 .add_segment(mock_ctx, version, sketch_id, segment, None)
7332 .await
7333 .unwrap();
7334 let line_id = *scene_graph_delta
7335 .new_objects
7336 .last()
7337 .expect("Expected line object id to be created");
7338
7339 (sketch_id, line_id, source_delta, scene_graph_delta)
7340 }
7341
7342 async fn seed_frontend_with_mock(frontend: &mut FrontendState, mock_ctx: &ExecutorContext, program: &Program) {
7343 frontend.program = program.clone();
7344 let outcome = mock_ctx.run_mock(program, &MockConfig::default()).await.unwrap();
7345 frontend.update_state_after_exec(outcome, true);
7346 }
7347
7348 #[test]
7349 fn test_parse_frontend_mutation_source_error_messages_are_user_facing() {
7350 for (source, expected_message) in [
7351 ("**", "Error parsing KCL source after editing: Unexpected token: *"),
7352 ("3'", "Error parsing KCL source after editing: found unknown token '''"),
7353 ] {
7354 let err = parse_frontend_mutation_source(
7355 source,
7356 "Error parsing KCL source after editing",
7357 "No AST produced after editing",
7358 )
7359 .expect_err("expected invalid KCL source to fail");
7360 let message = err.error.message();
7361
7362 assert_eq!(message, expected_message);
7363 assert!(!message.contains("CompilationIssue"));
7364 assert!(!message.contains("KclErrorDetails"));
7365 assert!(!message.contains("source_range"));
7366 }
7367 }
7368
7369 #[tokio::test(flavor = "multi_thread")]
7370 async fn test_edit_constraint_parse_error_messages_are_user_facing() {
7371 let initial_source = "\
7372sketch(on = XY) {
7373 line1 = line(start = [var 0, var 0], end = [var 10, var 0])
7374 distance([line1.start, line1.end]) == 10
7375}
7376";
7377 let program = Program::parse(initial_source).unwrap().0.unwrap();
7378
7379 let mut frontend = FrontendState::new();
7380 let mock_ctx = ExecutorContext::new_mock(None).await;
7381 let version = Version(0);
7382
7383 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
7384 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7385 let sketch_id = sketch_object.id;
7386 let sketch = expect_sketch(sketch_object);
7387 let constraint_id = *sketch.constraints.first().expect("expected distance constraint");
7388
7389 for (value, expected_message) in [
7390 ("**", "Invalid constraint value: Unexpected token: *"),
7391 ("3'", "Invalid constraint value: found unknown token '''"),
7392 ] {
7393 let err = frontend
7394 .edit_constraint(&mock_ctx, version, sketch_id, constraint_id, value.to_owned())
7395 .await
7396 .expect_err("expected invalid constraint expression to fail");
7397 let message = err.error.message();
7398
7399 assert_eq!(message, expected_message);
7400 assert!(!message.contains("CompilationIssue"));
7401 assert!(!message.contains("KclErrorDetails"));
7402 assert!(!message.contains("source_range"));
7403 }
7404
7405 mock_ctx.close().await;
7406 }
7407
7408 #[tokio::test(flavor = "multi_thread")]
7409 async fn test_failed_edit_constraint_does_not_update_program() {
7410 let initial_source = "\
7411sketch(on = XY) {
7412 line1 = line(start = [var 0, var 0], end = [var 10, var 0])
7413 distance([line1.start, line1.end]) == 10
7414}
7415";
7416 let program = Program::parse(initial_source).unwrap().0.unwrap();
7417 let original_source = program.original_file_contents.clone();
7418
7419 let mut frontend = FrontendState::new();
7420 let mock_ctx = ExecutorContext::new_mock(None).await;
7421 let version = Version(0);
7422
7423 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
7424 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7425 let sketch_id = sketch_object.id;
7426 let sketch = expect_sketch(sketch_object);
7427 let constraint_id = *sketch.constraints.first().expect("expected distance constraint");
7428
7429 frontend
7430 .edit_constraint(
7431 &mock_ctx,
7432 version,
7433 sketch_id,
7434 constraint_id,
7435 "unknownDistance".to_owned(),
7436 )
7437 .await
7438 .expect_err("expected invalid constraint value to fail execution");
7439
7440 assert_eq!(frontend.program.original_file_contents, original_source);
7441 assert!(!frontend.program.original_file_contents.contains("unknownDistance"));
7442
7443 mock_ctx.close().await;
7444 }
7445
7446 #[tokio::test(flavor = "multi_thread")]
7447 async fn test_edit_constraint_array_index_oob_fails_in_sketch_mode() {
7448 let initial_source = "\
7449arr = [0]
7450sketch(on = XY) {
7451 line1 = line(start = [var 0, var 0], end = [var 10, var 0])
7452 distance([line1.start, line1.end]) == 10
7453}
7454";
7455 let program = Program::parse(initial_source).unwrap().0.unwrap();
7456
7457 let mut frontend = FrontendState::new();
7458 let mock_ctx = ExecutorContext::new_mock(None).await;
7459 let version = Version(0);
7460
7461 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
7462 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7463 let sketch_id = sketch_object.id;
7464 let sketch = expect_sketch(sketch_object);
7465 let constraint_id = *sketch.constraints.first().expect("expected distance constraint");
7466
7467 let err = frontend
7471 .edit_constraint(&mock_ctx, version, sketch_id, constraint_id, "arr[5]".to_owned())
7472 .await
7473 .expect_err("expected out-of-bounds array index to be an error in sketch mode execution");
7474 let message = err.error.message();
7475 assert!(
7476 message.contains("The array doesn't have any item at index 5"),
7477 "unexpected error message: {message}"
7478 );
7479
7480 mock_ctx.close().await;
7481 }
7482
7483 #[tokio::test(flavor = "multi_thread")]
7484 async fn test_sketch_checkpoint_round_trip_restores_state() {
7485 let mut frontend = FrontendState::new();
7486 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7487 let mock_ctx = ExecutorContext::new_mock(None).await;
7488 let version = Version(0);
7489
7490 let (sketch_id, line_id, source_delta, scene_graph_delta) =
7491 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7492
7493 let expected_source = source_delta.text.clone();
7494 let expected_scene_graph = frontend.scene_graph.clone();
7495 let expected_exec_outcome = scene_graph_delta.exec_outcome.clone();
7496 let expected_point_freedom_cache = frontend.point_freedom_cache.clone();
7497
7498 let checkpoint_id = frontend
7499 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7500 .await
7501 .unwrap();
7502
7503 let edited_segments = vec![ExistingSegmentCtor {
7504 id: line_id,
7505 ctor: SegmentCtor::Line(make_line_ctor(1.0, 2.0, 13.0, 14.0, NumericSuffix::Mm)),
7506 }];
7507 let (edited_source, _edited_scene) = frontend
7508 .edit_segments(&mock_ctx, version, sketch_id, edited_segments)
7509 .await
7510 .unwrap();
7511 assert_ne!(edited_source.text, expected_source);
7512
7513 let restored = frontend.restore_sketch_checkpoint(checkpoint_id).await.unwrap();
7514
7515 assert_eq!(restored.source_delta.text, expected_source);
7516 assert_eq!(restored.scene_graph_delta.new_graph, expected_scene_graph);
7517 assert!(restored.scene_graph_delta.invalidates_ids);
7518 assert_eq!(restored.scene_graph_delta.exec_outcome, expected_exec_outcome);
7519 assert_eq!(frontend.scene_graph, expected_scene_graph);
7520 assert_eq!(frontend.point_freedom_cache, expected_point_freedom_cache);
7521
7522 ctx.close().await;
7523 }
7524
7525 #[tokio::test(flavor = "multi_thread")]
7526 async fn test_sketch_checkpoints_prune_oldest_entries() {
7527 let mut frontend = FrontendState::new();
7528 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7529 let mock_ctx = ExecutorContext::new_mock(None).await;
7530 let version = Version(0);
7531
7532 let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
7533 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7534
7535 let mut checkpoint_ids = Vec::new();
7536 for _ in 0..(MAX_SKETCH_CHECKPOINTS + 3) {
7537 checkpoint_ids.push(
7538 frontend
7539 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7540 .await
7541 .unwrap(),
7542 );
7543 }
7544
7545 assert_eq!(frontend.sketch_checkpoints.len(), MAX_SKETCH_CHECKPOINTS);
7546 assert!(checkpoint_ids.windows(2).all(|ids| ids[0] < ids[1]));
7547
7548 let oldest_retained = checkpoint_ids[3];
7549 assert_eq!(
7550 frontend.sketch_checkpoints.front().map(|checkpoint| checkpoint.id),
7551 Some(oldest_retained)
7552 );
7553
7554 let evicted_restore = frontend.restore_sketch_checkpoint(checkpoint_ids[0]).await;
7555 assert!(evicted_restore.is_err());
7556 assert!(evicted_restore.unwrap_err().msg.contains("Sketch checkpoint not found"));
7557
7558 frontend
7559 .restore_sketch_checkpoint(*checkpoint_ids.last().unwrap())
7560 .await
7561 .unwrap();
7562
7563 ctx.close().await;
7564 }
7565
7566 #[tokio::test(flavor = "multi_thread")]
7567 async fn test_restore_sketch_checkpoint_missing_id_returns_error() {
7568 let mut frontend = FrontendState::new();
7569 let missing_checkpoint = SketchCheckpointId::new(999);
7570
7571 let err = frontend
7572 .restore_sketch_checkpoint(missing_checkpoint)
7573 .await
7574 .expect_err("Expected restore to fail for missing checkpoint");
7575
7576 assert!(err.msg.contains("Sketch checkpoint not found"));
7577 }
7578
7579 #[tokio::test(flavor = "multi_thread")]
7580 async fn test_clear_sketch_checkpoints_removes_all_restore_points() {
7581 let mut frontend = FrontendState::new();
7582 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7583 let mock_ctx = ExecutorContext::new_mock(None).await;
7584 let version = Version(0);
7585
7586 let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
7587 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7588
7589 let checkpoint_a = frontend
7590 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7591 .await
7592 .unwrap();
7593 let checkpoint_b = frontend
7594 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7595 .await
7596 .unwrap();
7597 assert_eq!(frontend.sketch_checkpoints.len(), 2);
7598
7599 frontend.clear_sketch_checkpoints();
7600 assert!(frontend.sketch_checkpoints.is_empty());
7601 frontend.restore_sketch_checkpoint(checkpoint_a).await.unwrap_err();
7602 frontend.restore_sketch_checkpoint(checkpoint_b).await.unwrap_err();
7603
7604 ctx.close().await;
7605 }
7606
7607 #[tokio::test(flavor = "multi_thread")]
7608 async fn test_hack_set_program_keeps_old_checkpoints_and_adds_fresh_baseline() {
7609 let mut frontend = FrontendState::new();
7610 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7611 let mock_ctx = ExecutorContext::new_mock(None).await;
7612 let version = Version(0);
7613
7614 let (_sketch_id, _line_id, source_delta, scene_graph_delta) =
7615 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7616 let old_source = source_delta.text.clone();
7617 let old_checkpoint = frontend
7618 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7619 .await
7620 .unwrap();
7621 let initial_checkpoint_count = frontend.sketch_checkpoints.len();
7622
7623 let new_program = Program::parse("sketch(on = XY) {\n point(at = [1mm, 2mm])\n}\n")
7624 .unwrap()
7625 .0
7626 .unwrap();
7627
7628 let result = frontend.hack_set_program(&ctx, new_program).await.unwrap();
7629 let SetProgramOutcome::Success {
7630 checkpoint_id: Some(new_checkpoint),
7631 ..
7632 } = result
7633 else {
7634 panic!("Expected Success with a fresh checkpoint baseline");
7635 };
7636
7637 assert_eq!(frontend.sketch_checkpoints.len(), initial_checkpoint_count + 1);
7638
7639 let old_restore = frontend.restore_sketch_checkpoint(old_checkpoint).await.unwrap();
7640 assert_eq!(old_restore.source_delta.text, old_source);
7641
7642 let new_restore = frontend.restore_sketch_checkpoint(new_checkpoint).await.unwrap();
7643 assert!(new_restore.source_delta.text.contains("point(at = [1mm, 2mm])"));
7644
7645 ctx.close().await;
7646 }
7647
7648 #[tokio::test(flavor = "multi_thread")]
7649 async fn test_hack_set_program_exec_failure_does_not_add_checkpoint() {
7650 let mut frontend = FrontendState::new();
7651 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7652 let mock_ctx = ExecutorContext::new_mock(None).await;
7653 let version = Version(0);
7654
7655 let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
7656 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7657 let old_checkpoint = frontend
7658 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7659 .await
7660 .unwrap();
7661 let checkpoint_count_before = frontend.sketch_checkpoints.len();
7662
7663 let failing_program = Program::parse(
7664 "sketch(on = XY) {\n line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])\n}\n\nbad = missing_name\n",
7665 )
7666 .unwrap()
7667 .0
7668 .unwrap();
7669
7670 let result = frontend.hack_set_program(&ctx, failing_program).await.unwrap();
7671 assert!(matches!(result, SetProgramOutcome::ExecFailure { .. }));
7672 assert_eq!(frontend.sketch_checkpoints.len(), checkpoint_count_before);
7673 frontend.restore_sketch_checkpoint(old_checkpoint).await.unwrap();
7674
7675 ctx.close().await;
7676 }
7677
7678 #[tokio::test(flavor = "multi_thread")]
7679 async fn test_restore_sketch_checkpoint_restores_and_clears_mock_memory() {
7680 let mut frontend = FrontendState::new();
7681 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7682
7683 let program = Program::parse(
7684 "width = 2mm\nsketch001 = sketch(on = offsetPlane(XY, offset = width)) {\n line1 = line(start = [var 0, var 0], end = [var 1mm, var 0])\n distance([line1.start, line1.end]) == width\n}\n",
7685 )
7686 .unwrap()
7687 .0
7688 .unwrap();
7689 let set_program_outcome = frontend.hack_set_program(&ctx, program).await.unwrap();
7690 let SetProgramOutcome::Success { exec_outcome, .. } = set_program_outcome else {
7691 panic!("Expected successful baseline program execution");
7692 };
7693
7694 clear_mem_cache().await;
7695 assert!(read_old_memory().await.is_none());
7696
7697 let checkpoint_without_mock_memory = frontend
7698 .create_sketch_checkpoint((*exec_outcome).clone())
7699 .await
7700 .unwrap();
7701
7702 write_old_memory(SketchModeState::new_for_tests()).await;
7703 assert!(read_old_memory().await.is_some());
7704
7705 let checkpoint_with_mock_memory = frontend
7706 .create_sketch_checkpoint((*exec_outcome).clone())
7707 .await
7708 .unwrap();
7709
7710 clear_mem_cache().await;
7711 assert!(read_old_memory().await.is_none());
7712
7713 frontend
7714 .restore_sketch_checkpoint(checkpoint_with_mock_memory)
7715 .await
7716 .unwrap();
7717 assert!(read_old_memory().await.is_some());
7718
7719 frontend
7720 .restore_sketch_checkpoint(checkpoint_without_mock_memory)
7721 .await
7722 .unwrap();
7723 assert!(read_old_memory().await.is_none());
7724
7725 ctx.close().await;
7726 }
7727
7728 #[tokio::test(flavor = "multi_thread")]
7729 async fn test_hack_set_program_exec_error_still_allows_edit_sketch() {
7730 let source = "\
7731sketch(on = XY) {
7732 line1 = line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])
7733}
7734
7735bad = missing_name
7736";
7737 let program = Program::parse(source).unwrap().0.unwrap();
7738
7739 let mut frontend = FrontendState::new();
7740
7741 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7742 let mock_ctx = ExecutorContext::new_mock(None).await;
7743 let version = Version(0);
7744 let project_id = ProjectId(0);
7745 let file_id = FileId(0);
7746
7747 let SetProgramOutcome::ExecFailure { .. } = frontend.hack_set_program(&ctx, program).await.unwrap() else {
7748 panic!("Expected ExecFailure from hack_set_program due to syntax error in program");
7749 };
7750
7751 let sketch_id = frontend
7752 .scene_graph
7753 .objects
7754 .iter()
7755 .find_map(|obj| matches!(obj.kind, ObjectKind::Sketch(_)).then_some(obj.id))
7756 .expect("Expected sketch object from errored hack_set_program");
7757
7758 frontend
7759 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
7760 .await
7761 .unwrap();
7762
7763 ctx.close().await;
7764 mock_ctx.close().await;
7765 }
7766
7767 #[tokio::test(flavor = "multi_thread")]
7768 async fn test_new_sketch_add_point_edit_point() {
7769 let program = Program::empty();
7770
7771 let mut frontend = FrontendState::new();
7772 frontend.program = program;
7773
7774 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7775 let mock_ctx = ExecutorContext::new_mock(None).await;
7776 let version = Version(0);
7777
7778 let sketch_args = SketchCtor {
7779 on: Plane::Default(PlaneName::Xy),
7780 };
7781 let (_src_delta, scene_delta, sketch_id) = frontend
7782 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
7783 .await
7784 .unwrap();
7785 assert_eq!(sketch_id, ObjectId(1));
7786 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
7787 let sketch_object = &scene_delta.new_graph.objects[1];
7788 assert_eq!(sketch_object.id, ObjectId(1));
7789 assert_eq!(
7790 sketch_object.kind,
7791 ObjectKind::Sketch(Sketch {
7792 args: SketchCtor {
7793 on: Plane::Default(PlaneName::Xy)
7794 },
7795 plane: ObjectId(0),
7796 segments: vec![],
7797 constraints: vec![],
7798 })
7799 );
7800 assert_eq!(scene_delta.new_graph.objects.len(), 2);
7801
7802 let point_ctor = PointCtor {
7803 position: Point2d {
7804 x: Expr::Number(Number {
7805 value: 1.0,
7806 units: NumericSuffix::Inch,
7807 }),
7808 y: Expr::Number(Number {
7809 value: 2.0,
7810 units: NumericSuffix::Inch,
7811 }),
7812 },
7813 };
7814 let segment = SegmentCtor::Point(point_ctor);
7815 let (src_delta, scene_delta) = frontend
7816 .add_segment(&mock_ctx, version, sketch_id, segment, None)
7817 .await
7818 .unwrap();
7819 insta::assert_snapshot!("test_new_sketch_add_point_edit_point_1", src_delta.text.as_str());
7820 assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
7821 assert_eq!(scene_delta.new_graph.objects.len(), 3);
7822 for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
7823 assert_eq!(scene_object.id.0, i);
7824 }
7825
7826 let point_id = *scene_delta.new_objects.last().unwrap();
7827
7828 let point_ctor = PointCtor {
7829 position: Point2d {
7830 x: Expr::Number(Number {
7831 value: 3.0,
7832 units: NumericSuffix::Inch,
7833 }),
7834 y: Expr::Number(Number {
7835 value: 4.0,
7836 units: NumericSuffix::Inch,
7837 }),
7838 },
7839 };
7840 let segments = vec![ExistingSegmentCtor {
7841 id: point_id,
7842 ctor: SegmentCtor::Point(point_ctor),
7843 }];
7844 let (src_delta, scene_delta) = frontend
7845 .edit_segments(&mock_ctx, version, sketch_id, segments)
7846 .await
7847 .unwrap();
7848 insta::assert_snapshot!("test_new_sketch_add_point_edit_point_2", src_delta.text.as_str());
7849 assert_eq!(scene_delta.new_objects, vec![]);
7850 assert_eq!(scene_delta.new_graph.objects.len(), 3);
7851
7852 ctx.close().await;
7853 mock_ctx.close().await;
7854 }
7855
7856 #[tokio::test(flavor = "multi_thread")]
7857 async fn test_new_sketch_add_line_edit_line() {
7858 let program = Program::empty();
7859
7860 let mut frontend = FrontendState::new();
7861 frontend.program = program;
7862
7863 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7864 let mock_ctx = ExecutorContext::new_mock(None).await;
7865 let version = Version(0);
7866
7867 let sketch_args = SketchCtor {
7868 on: Plane::Default(PlaneName::Xy),
7869 };
7870 let (_src_delta, scene_delta, sketch_id) = frontend
7871 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
7872 .await
7873 .unwrap();
7874 assert_eq!(sketch_id, ObjectId(1));
7875 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
7876 let sketch_object = &scene_delta.new_graph.objects[1];
7877 assert_eq!(sketch_object.id, ObjectId(1));
7878 assert_eq!(
7879 sketch_object.kind,
7880 ObjectKind::Sketch(Sketch {
7881 args: SketchCtor {
7882 on: Plane::Default(PlaneName::Xy)
7883 },
7884 plane: ObjectId(0),
7885 segments: vec![],
7886 constraints: vec![],
7887 })
7888 );
7889 assert_eq!(scene_delta.new_graph.objects.len(), 2);
7890
7891 let line_ctor = LineCtor {
7892 start: Point2d {
7893 x: Expr::Number(Number {
7894 value: 0.0,
7895 units: NumericSuffix::Mm,
7896 }),
7897 y: Expr::Number(Number {
7898 value: 0.0,
7899 units: NumericSuffix::Mm,
7900 }),
7901 },
7902 end: Point2d {
7903 x: Expr::Number(Number {
7904 value: 10.0,
7905 units: NumericSuffix::Mm,
7906 }),
7907 y: Expr::Number(Number {
7908 value: 10.0,
7909 units: NumericSuffix::Mm,
7910 }),
7911 },
7912 construction: None,
7913 };
7914 let segment = SegmentCtor::Line(line_ctor);
7915 let (src_delta, scene_delta) = frontend
7916 .add_segment(&mock_ctx, version, sketch_id, segment, None)
7917 .await
7918 .unwrap();
7919 insta::assert_snapshot!("test_new_sketch_add_line_edit_line_1", src_delta.text.as_str());
7920 assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
7921 assert_eq!(scene_delta.new_graph.objects.len(), 5);
7922 for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
7923 assert_eq!(scene_object.id.0, i);
7924 }
7925
7926 let line = *scene_delta.new_objects.last().unwrap();
7928
7929 let line_ctor = LineCtor {
7930 start: Point2d {
7931 x: Expr::Number(Number {
7932 value: 1.0,
7933 units: NumericSuffix::Mm,
7934 }),
7935 y: Expr::Number(Number {
7936 value: 2.0,
7937 units: NumericSuffix::Mm,
7938 }),
7939 },
7940 end: Point2d {
7941 x: Expr::Number(Number {
7942 value: 13.0,
7943 units: NumericSuffix::Mm,
7944 }),
7945 y: Expr::Number(Number {
7946 value: 14.0,
7947 units: NumericSuffix::Mm,
7948 }),
7949 },
7950 construction: None,
7951 };
7952 let segments = vec![ExistingSegmentCtor {
7953 id: line,
7954 ctor: SegmentCtor::Line(line_ctor),
7955 }];
7956 let (src_delta, scene_delta) = frontend
7957 .edit_segments(&mock_ctx, version, sketch_id, segments)
7958 .await
7959 .unwrap();
7960 insta::assert_snapshot!("test_new_sketch_add_line_edit_line_2", src_delta.text.as_str());
7961 assert_eq!(scene_delta.new_objects, vec![]);
7962 assert_eq!(scene_delta.new_graph.objects.len(), 5);
7963
7964 ctx.close().await;
7965 mock_ctx.close().await;
7966 }
7967
7968 #[tokio::test(flavor = "multi_thread")]
7969 async fn test_new_sketch_add_arc_edit_arc() {
7970 let program = Program::empty();
7971
7972 let mut frontend = FrontendState::new();
7973 frontend.program = program;
7974
7975 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7976 let mock_ctx = ExecutorContext::new_mock(None).await;
7977 let version = Version(0);
7978
7979 let sketch_args = SketchCtor {
7980 on: Plane::Default(PlaneName::Xy),
7981 };
7982 let (_src_delta, scene_delta, sketch_id) = frontend
7983 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
7984 .await
7985 .unwrap();
7986 assert_eq!(sketch_id, ObjectId(1));
7987 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
7988 let sketch_object = &scene_delta.new_graph.objects[1];
7989 assert_eq!(sketch_object.id, ObjectId(1));
7990 assert_eq!(
7991 sketch_object.kind,
7992 ObjectKind::Sketch(Sketch {
7993 args: SketchCtor {
7994 on: Plane::Default(PlaneName::Xy),
7995 },
7996 plane: ObjectId(0),
7997 segments: vec![],
7998 constraints: vec![],
7999 })
8000 );
8001 assert_eq!(scene_delta.new_graph.objects.len(), 2);
8002
8003 let arc_ctor = ArcCtor {
8004 start: Point2d {
8005 x: Expr::Var(Number {
8006 value: 0.0,
8007 units: NumericSuffix::Mm,
8008 }),
8009 y: Expr::Var(Number {
8010 value: 0.0,
8011 units: NumericSuffix::Mm,
8012 }),
8013 },
8014 end: Point2d {
8015 x: Expr::Var(Number {
8016 value: 10.0,
8017 units: NumericSuffix::Mm,
8018 }),
8019 y: Expr::Var(Number {
8020 value: 10.0,
8021 units: NumericSuffix::Mm,
8022 }),
8023 },
8024 center: Point2d {
8025 x: Expr::Var(Number {
8026 value: 10.0,
8027 units: NumericSuffix::Mm,
8028 }),
8029 y: Expr::Var(Number {
8030 value: 0.0,
8031 units: NumericSuffix::Mm,
8032 }),
8033 },
8034 construction: None,
8035 };
8036 let segment = SegmentCtor::Arc(arc_ctor);
8037 let (src_delta, scene_delta) = frontend
8038 .add_segment(&mock_ctx, version, sketch_id, segment, None)
8039 .await
8040 .unwrap();
8041 insta::assert_snapshot!("test_new_sketch_add_arc_edit_arc_1", src_delta.text.as_str());
8042 assert_eq!(
8043 scene_delta.new_objects,
8044 vec![ObjectId(2), ObjectId(3), ObjectId(4), ObjectId(5)]
8045 );
8046 for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
8047 assert_eq!(scene_object.id.0, i);
8048 }
8049 assert_eq!(scene_delta.new_graph.objects.len(), 6);
8050
8051 let arc = *scene_delta.new_objects.last().unwrap();
8053
8054 let arc_ctor = ArcCtor {
8055 start: Point2d {
8056 x: Expr::Var(Number {
8057 value: 1.0,
8058 units: NumericSuffix::Mm,
8059 }),
8060 y: Expr::Var(Number {
8061 value: 2.0,
8062 units: NumericSuffix::Mm,
8063 }),
8064 },
8065 end: Point2d {
8066 x: Expr::Var(Number {
8067 value: 13.0,
8068 units: NumericSuffix::Mm,
8069 }),
8070 y: Expr::Var(Number {
8071 value: 14.0,
8072 units: NumericSuffix::Mm,
8073 }),
8074 },
8075 center: Point2d {
8076 x: Expr::Var(Number {
8077 value: 13.0,
8078 units: NumericSuffix::Mm,
8079 }),
8080 y: Expr::Var(Number {
8081 value: 2.0,
8082 units: NumericSuffix::Mm,
8083 }),
8084 },
8085 construction: None,
8086 };
8087 let segments = vec![ExistingSegmentCtor {
8088 id: arc,
8089 ctor: SegmentCtor::Arc(arc_ctor),
8090 }];
8091 let (src_delta, scene_delta) = frontend
8092 .edit_segments(&mock_ctx, version, sketch_id, segments)
8093 .await
8094 .unwrap();
8095 insta::assert_snapshot!("test_new_sketch_add_arc_edit_arc_2", src_delta.text.as_str());
8096 assert_eq!(scene_delta.new_objects, vec![]);
8097 assert_eq!(scene_delta.new_graph.objects.len(), 6);
8098
8099 ctx.close().await;
8100 mock_ctx.close().await;
8101 }
8102
8103 #[tokio::test(flavor = "multi_thread")]
8104 async fn test_new_sketch_add_circle_edit_circle() {
8105 let program = Program::empty();
8106
8107 let mut frontend = FrontendState::new();
8108 frontend.program = program;
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 let sketch_args = SketchCtor {
8115 on: Plane::Default(PlaneName::Xy),
8116 };
8117 let (_src_delta, _scene_delta, sketch_id) = frontend
8118 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
8119 .await
8120 .unwrap();
8121
8122 let circle_ctor = CircleCtor {
8124 start: Point2d {
8125 x: Expr::Var(Number {
8126 value: 5.0,
8127 units: NumericSuffix::Mm,
8128 }),
8129 y: Expr::Var(Number {
8130 value: 0.0,
8131 units: NumericSuffix::Mm,
8132 }),
8133 },
8134 center: Point2d {
8135 x: Expr::Var(Number {
8136 value: 0.0,
8137 units: NumericSuffix::Mm,
8138 }),
8139 y: Expr::Var(Number {
8140 value: 0.0,
8141 units: NumericSuffix::Mm,
8142 }),
8143 },
8144 construction: None,
8145 };
8146 let segment = SegmentCtor::Circle(circle_ctor);
8147 let (src_delta, scene_delta) = frontend
8148 .add_segment(&mock_ctx, version, sketch_id, segment, None)
8149 .await
8150 .unwrap();
8151 insta::assert_snapshot!("test_new_sketch_add_circle_edit_circle_1", src_delta.text.as_str());
8152 assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
8154 assert_eq!(scene_delta.new_graph.objects.len(), 5);
8155
8156 let circle = *scene_delta.new_objects.last().unwrap();
8157
8158 let circle_ctor = CircleCtor {
8160 start: Point2d {
8161 x: Expr::Var(Number {
8162 value: 10.0,
8163 units: NumericSuffix::Mm,
8164 }),
8165 y: Expr::Var(Number {
8166 value: 0.0,
8167 units: NumericSuffix::Mm,
8168 }),
8169 },
8170 center: Point2d {
8171 x: Expr::Var(Number {
8172 value: 3.0,
8173 units: NumericSuffix::Mm,
8174 }),
8175 y: Expr::Var(Number {
8176 value: 4.0,
8177 units: NumericSuffix::Mm,
8178 }),
8179 },
8180 construction: None,
8181 };
8182 let segments = vec![ExistingSegmentCtor {
8183 id: circle,
8184 ctor: SegmentCtor::Circle(circle_ctor),
8185 }];
8186 let (src_delta, scene_delta) = frontend
8187 .edit_segments(&mock_ctx, version, sketch_id, segments)
8188 .await
8189 .unwrap();
8190 insta::assert_snapshot!("test_new_sketch_add_circle_edit_circle_2", src_delta.text.as_str());
8191 assert_eq!(scene_delta.new_objects, vec![]);
8192 assert_eq!(scene_delta.new_graph.objects.len(), 5);
8193
8194 ctx.close().await;
8195 mock_ctx.close().await;
8196 }
8197
8198 #[tokio::test(flavor = "multi_thread")]
8199 async fn test_delete_circle() {
8200 let initial_source = "sketch001 = sketch(on = XY) {
8201 circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
8202}
8203";
8204
8205 let program = Program::parse(initial_source).unwrap().0.unwrap();
8206 let mut frontend = FrontendState::new();
8207
8208 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8209 let mock_ctx = ExecutorContext::new_mock(None).await;
8210 let version = Version(0);
8211
8212 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8213 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8214 let sketch_id = sketch_object.id;
8215 let sketch = expect_sketch(sketch_object);
8216
8217 assert_eq!(sketch.segments.len(), 3);
8219 let circle_id = sketch.segments[2];
8220
8221 let (src_delta, scene_delta) = frontend
8223 .delete_objects(&mock_ctx, version, sketch_id, vec![], vec![circle_id])
8224 .await
8225 .unwrap();
8226 insta::assert_snapshot!("test_delete_circle", src_delta.text.as_str());
8227 let new_sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8228 let new_sketch = expect_sketch(new_sketch_object);
8229 assert_eq!(new_sketch.segments.len(), 0);
8230
8231 ctx.close().await;
8232 mock_ctx.close().await;
8233 }
8234
8235 #[tokio::test(flavor = "multi_thread")]
8236 async fn test_edit_circle_via_point() {
8237 let initial_source = "sketch001 = sketch(on = XY) {
8238 circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
8239}
8240";
8241
8242 let program = Program::parse(initial_source).unwrap().0.unwrap();
8243 let mut frontend = FrontendState::new();
8244
8245 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8246 let mock_ctx = ExecutorContext::new_mock(None).await;
8247 let version = Version(0);
8248
8249 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8250 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8251 let sketch_id = sketch_object.id;
8252 let sketch = expect_sketch(sketch_object);
8253
8254 let circle_id = sketch
8256 .segments
8257 .iter()
8258 .copied()
8259 .find(|seg_id| {
8260 matches!(
8261 &frontend.scene_graph.objects[seg_id.0].kind,
8262 ObjectKind::Segment {
8263 segment: Segment::Circle(_)
8264 }
8265 )
8266 })
8267 .expect("Expected a circle segment in sketch");
8268 let circle_object = &frontend.scene_graph.objects[circle_id.0];
8269 let ObjectKind::Segment {
8270 segment: Segment::Circle(circle),
8271 } = &circle_object.kind
8272 else {
8273 panic!("Expected circle segment, got: {:?}", circle_object.kind);
8274 };
8275 let start_point_id = circle.start;
8276
8277 let segments = vec![ExistingSegmentCtor {
8279 id: start_point_id,
8280 ctor: SegmentCtor::Point(PointCtor {
8281 position: Point2d {
8282 x: Expr::Var(Number {
8283 value: 7.0,
8284 units: NumericSuffix::Mm,
8285 }),
8286 y: Expr::Var(Number {
8287 value: 1.0,
8288 units: NumericSuffix::Mm,
8289 }),
8290 },
8291 }),
8292 }];
8293 let (src_delta, _scene_delta) = frontend
8294 .edit_segments(&mock_ctx, version, sketch_id, segments)
8295 .await
8296 .unwrap();
8297 insta::assert_snapshot!("test_edit_circle_via_point", src_delta.text.as_str());
8298
8299 ctx.close().await;
8300 mock_ctx.close().await;
8301 }
8302
8303 #[tokio::test(flavor = "multi_thread")]
8304 async fn test_add_line_when_sketch_block_uses_variable() {
8305 let initial_source = "s = sketch(on = XY) {}
8306";
8307
8308 let program = Program::parse(initial_source).unwrap().0.unwrap();
8309
8310 let mut frontend = FrontendState::new();
8311
8312 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8313 let mock_ctx = ExecutorContext::new_mock(None).await;
8314 let version = Version(0);
8315
8316 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8317 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8318 let sketch_id = sketch_object.id;
8319
8320 let line_ctor = LineCtor {
8321 start: Point2d {
8322 x: Expr::Number(Number {
8323 value: 0.0,
8324 units: NumericSuffix::Mm,
8325 }),
8326 y: Expr::Number(Number {
8327 value: 0.0,
8328 units: NumericSuffix::Mm,
8329 }),
8330 },
8331 end: Point2d {
8332 x: Expr::Number(Number {
8333 value: 10.0,
8334 units: NumericSuffix::Mm,
8335 }),
8336 y: Expr::Number(Number {
8337 value: 10.0,
8338 units: NumericSuffix::Mm,
8339 }),
8340 },
8341 construction: None,
8342 };
8343 let segment = SegmentCtor::Line(line_ctor);
8344 let (src_delta, scene_delta) = frontend
8345 .add_segment(&mock_ctx, version, sketch_id, segment, None)
8346 .await
8347 .unwrap();
8348 insta::assert_snapshot!("test_add_line_when_sketch_block_uses_variable", src_delta.text.as_str());
8349 assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
8350 assert_eq!(scene_delta.new_graph.objects.len(), 5);
8351
8352 ctx.close().await;
8353 mock_ctx.close().await;
8354 }
8355
8356 #[tokio::test(flavor = "multi_thread")]
8357 async fn test_new_sketch_add_line_delete_sketch() {
8358 let program = Program::empty();
8359
8360 let mut frontend = FrontendState::new();
8361 frontend.program = program;
8362
8363 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8364 let mock_ctx = ExecutorContext::new_mock(None).await;
8365 let version = Version(0);
8366
8367 let sketch_args = SketchCtor {
8368 on: Plane::Default(PlaneName::Xy),
8369 };
8370 let (_src_delta, scene_delta, sketch_id) = frontend
8371 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
8372 .await
8373 .unwrap();
8374 assert_eq!(sketch_id, ObjectId(1));
8375 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
8376 let sketch_object = &scene_delta.new_graph.objects[1];
8377 assert_eq!(sketch_object.id, ObjectId(1));
8378 assert_eq!(
8379 sketch_object.kind,
8380 ObjectKind::Sketch(Sketch {
8381 args: SketchCtor {
8382 on: Plane::Default(PlaneName::Xy)
8383 },
8384 plane: ObjectId(0),
8385 segments: vec![],
8386 constraints: vec![],
8387 })
8388 );
8389 assert_eq!(scene_delta.new_graph.objects.len(), 2);
8390
8391 let line_ctor = LineCtor {
8392 start: Point2d {
8393 x: Expr::Number(Number {
8394 value: 0.0,
8395 units: NumericSuffix::Mm,
8396 }),
8397 y: Expr::Number(Number {
8398 value: 0.0,
8399 units: NumericSuffix::Mm,
8400 }),
8401 },
8402 end: Point2d {
8403 x: Expr::Number(Number {
8404 value: 10.0,
8405 units: NumericSuffix::Mm,
8406 }),
8407 y: Expr::Number(Number {
8408 value: 10.0,
8409 units: NumericSuffix::Mm,
8410 }),
8411 },
8412 construction: None,
8413 };
8414 let segment = SegmentCtor::Line(line_ctor);
8415 let (src_delta, scene_delta) = frontend
8416 .add_segment(&mock_ctx, version, sketch_id, segment, None)
8417 .await
8418 .unwrap();
8419 insta::assert_snapshot!("test_new_sketch_add_line_delete_sketch_1", src_delta.text.as_str());
8420 assert_eq!(scene_delta.new_graph.objects.len(), 5);
8421
8422 let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8423 insta::assert_snapshot!("test_new_sketch_add_line_delete_sketch_2", src_delta.text.as_str());
8424 assert_eq!(scene_delta.new_graph.objects.len(), 0);
8425
8426 ctx.close().await;
8427 mock_ctx.close().await;
8428 }
8429
8430 #[tokio::test(flavor = "multi_thread")]
8431 async fn test_delete_sketch_when_sketch_block_uses_variable() {
8432 let initial_source = "s = sketch(on = XY) {}
8433";
8434
8435 let program = Program::parse(initial_source).unwrap().0.unwrap();
8436
8437 let mut frontend = FrontendState::new();
8438
8439 let ctx = ExecutorContext::new_with_engine(sync::Arc::new(EngineManager::new_mock()), Default::default());
8440 let version = Version(0);
8441
8442 frontend.hack_set_program(&ctx, program).await.unwrap();
8443 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8444 let sketch_id = sketch_object.id;
8445
8446 let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8447 insta::assert_snapshot!(
8448 "test_delete_sketch_when_sketch_block_uses_variable",
8449 src_delta.text.as_str()
8450 );
8451 assert_eq!(scene_delta.new_graph.objects.len(), 0);
8452
8453 ctx.close().await;
8454 }
8455
8456 #[tokio::test(flavor = "multi_thread")]
8457 async fn test_delete_sketch_after_comment() {
8458 let initial_source = "sketch001 = sketch(on = XZ) {
8459}
8460";
8461
8462 let program = Program::parse(initial_source).unwrap().0.unwrap();
8463 let mut frontend = FrontendState::new();
8464
8465 let ctx = ExecutorContext::new_with_engine(sync::Arc::new(EngineManager::new_mock()), Default::default());
8466 let version = Version(0);
8467
8468 frontend.hack_set_program(&ctx, program).await.unwrap();
8469 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8470 let sketch_id = sketch_object.id;
8471 let original_source = sketch_object.source.clone();
8472
8473 let commented_source = "// test 1
8474sketch001 = sketch(on = XZ) {
8475}
8476";
8477 let commented_program = Program::parse(commented_source).unwrap().0.unwrap();
8478 frontend.engine_execute(&ctx, commented_program).await.unwrap();
8479
8480 let cached_sketch_object = &frontend.scene_graph.objects[sketch_id.0];
8481 assert_eq!(cached_sketch_object.source, original_source);
8482
8483 let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8484 assert!(
8485 !src_delta.text.contains("sketch001"),
8486 "sketch was not deleted: {}",
8487 src_delta.text
8488 );
8489 insta::assert_snapshot!("test_delete_sketch_after_comment", src_delta.text.as_str());
8491 assert_eq!(scene_delta.new_graph.objects.len(), 0);
8492
8493 ctx.close().await;
8494 }
8495
8496 #[tokio::test(flavor = "multi_thread")]
8497 async fn test_delete_sketch_preserves_pre_comment_when_followed_by_code() {
8498 let initial_source = "sketch001 = sketch(on = XZ) {
8499}
8500foo = 1
8501";
8502
8503 let program = Program::parse(initial_source).unwrap().0.unwrap();
8504 let mut frontend = FrontendState::new();
8505
8506 let ctx = ExecutorContext::new_with_engine(sync::Arc::new(EngineManager::new_mock()), Default::default());
8507 let version = Version(0);
8508
8509 frontend.hack_set_program(&ctx, program).await.unwrap();
8510 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8511 let sketch_id = sketch_object.id;
8512
8513 let commented_source = "// keep me
8514sketch001 = sketch(on = XZ) {
8515}
8516foo = 1
8517";
8518 let commented_program = Program::parse(commented_source).unwrap().0.unwrap();
8519 frontend.engine_execute(&ctx, commented_program).await.unwrap();
8520
8521 let (src_delta, _scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8522 insta::assert_snapshot!(
8524 "test_delete_sketch_preserves_pre_comment_when_followed_by_code",
8525 src_delta.text.as_str()
8526 );
8527
8528 ctx.close().await;
8529 }
8530
8531 #[tokio::test(flavor = "multi_thread")]
8532 async fn test_delete_segment_preserves_pre_comment() {
8533 let initial_source = "\
8534sketch(on = XY) {
8535 point(at = [var 1, var 2])
8536 // describe the middle point
8537 point(at = [var 3, var 4])
8538 point(at = [var 5, var 6])
8539}
8540";
8541
8542 let program = Program::parse(initial_source).unwrap().0.unwrap();
8543 let mut frontend = FrontendState::new();
8544
8545 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8546 let mock_ctx = ExecutorContext::new_mock(None).await;
8547 let version = Version(0);
8548
8549 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8550 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8551 let sketch_id = sketch_object.id;
8552 let sketch = expect_sketch(sketch_object);
8553
8554 let middle_point_id = *sketch.segments.get(1).unwrap();
8555
8556 let (src_delta, _scene_delta) = frontend
8557 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![middle_point_id])
8558 .await
8559 .unwrap();
8560 insta::assert_snapshot!("test_delete_segment_preserves_pre_comment", src_delta.text.as_str());
8563
8564 ctx.close().await;
8565 mock_ctx.close().await;
8566 }
8567
8568 #[tokio::test(flavor = "multi_thread")]
8569 async fn test_delete_last_segment_preserves_pre_comment() {
8570 let initial_source = "\
8571sketch(on = XY) {
8572 point(at = [var 1, var 2])
8573 // describe the trailing point
8574 point(at = [var 3, var 4])
8575}
8576";
8577
8578 let program = Program::parse(initial_source).unwrap().0.unwrap();
8579 let mut frontend = FrontendState::new();
8580
8581 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8582 let mock_ctx = ExecutorContext::new_mock(None).await;
8583 let version = Version(0);
8584
8585 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8586 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8587 let sketch_id = sketch_object.id;
8588 let sketch = expect_sketch(sketch_object);
8589
8590 let last_point_id = *sketch.segments.last().unwrap();
8591
8592 let (src_delta, _scene_delta) = frontend
8593 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![last_point_id])
8594 .await
8595 .unwrap();
8596 insta::assert_snapshot!(
8599 "test_delete_last_segment_preserves_pre_comment",
8600 src_delta.text.as_str()
8601 );
8602
8603 ctx.close().await;
8604 mock_ctx.close().await;
8605 }
8606
8607 #[tokio::test(flavor = "multi_thread")]
8608 async fn test_delete_segment_drops_inline_trailing_comment() {
8609 let initial_source = "\
8610sketch(on = XY) {
8611 point(at = [var 1, var 2])
8612 point(at = [var 3, var 4]) // same-line note that gets dropped
8613 point(at = [var 5, var 6])
8614}
8615";
8616
8617 let program = Program::parse(initial_source).unwrap().0.unwrap();
8618 let mut frontend = FrontendState::new();
8619
8620 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8621 let mock_ctx = ExecutorContext::new_mock(None).await;
8622 let version = Version(0);
8623
8624 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8625 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8626 let sketch_id = sketch_object.id;
8627 let sketch = expect_sketch(sketch_object);
8628
8629 let middle_point_id = *sketch.segments.get(1).unwrap();
8630
8631 let (src_delta, _scene_delta) = frontend
8632 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![middle_point_id])
8633 .await
8634 .unwrap();
8635 assert!(
8637 !src_delta.text.contains("same-line note"),
8638 "inline comment should have been removed: {}",
8639 src_delta.text
8640 );
8641
8642 ctx.close().await;
8643 mock_ctx.close().await;
8644 }
8645
8646 #[tokio::test(flavor = "multi_thread")]
8647 async fn test_delete_segments_preserves_block_comments_across_positions() {
8648 let initial_source = "\
8656sketch(on = XY) {
8657 /* above first - moves to middle */
8658 point(at = [var 1, var 2]) /* same-line on first - dropped */
8659 /* above middle - stays */
8660 point(at = [var 3, var 4])
8661 /* above last - moves to trailing meta */
8662 point(at = [var 5, var 6])
8663}
8664";
8665
8666 let program = Program::parse(initial_source).unwrap().0.unwrap();
8667 let mut frontend = FrontendState::new();
8668
8669 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8670 let mock_ctx = ExecutorContext::new_mock(None).await;
8671 let version = Version(0);
8672
8673 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8674 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8675 let sketch_id = sketch_object.id;
8676 let sketch = expect_sketch(sketch_object);
8677
8678 let first_point_id = *sketch.segments.first().unwrap();
8679 let last_point_id = *sketch.segments.last().unwrap();
8680
8681 let (src_delta, _scene_delta) = frontend
8682 .delete_objects(
8683 &mock_ctx,
8684 version,
8685 sketch_id,
8686 Vec::new(),
8687 vec![first_point_id, last_point_id],
8688 )
8689 .await
8690 .unwrap();
8691 insta::assert_snapshot!(
8692 "test_delete_segments_preserves_block_comments_across_positions",
8693 src_delta.text.as_str()
8694 );
8695
8696 ctx.close().await;
8697 mock_ctx.close().await;
8698 }
8699
8700 #[tokio::test(flavor = "multi_thread")]
8701 async fn test_edit_line_when_editing_its_start_point() {
8702 let initial_source = "\
8703sketch(on = XY) {
8704 line(start = [var 1, var 2], end = [var 3, var 4])
8705}
8706";
8707
8708 let program = Program::parse(initial_source).unwrap().0.unwrap();
8709
8710 let mut frontend = FrontendState::new();
8711
8712 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8713 let mock_ctx = ExecutorContext::new_mock(None).await;
8714 let version = Version(0);
8715
8716 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8717 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8718 let sketch_id = sketch_object.id;
8719 let sketch = expect_sketch(sketch_object);
8720
8721 let point_id = *sketch.segments.first().unwrap();
8722
8723 let point_ctor = PointCtor {
8724 position: Point2d {
8725 x: Expr::Var(Number {
8726 value: 5.0,
8727 units: NumericSuffix::Inch,
8728 }),
8729 y: Expr::Var(Number {
8730 value: 6.0,
8731 units: NumericSuffix::Inch,
8732 }),
8733 },
8734 };
8735 let segments = vec![ExistingSegmentCtor {
8736 id: point_id,
8737 ctor: SegmentCtor::Point(point_ctor),
8738 }];
8739 let (src_delta, scene_delta) = frontend
8740 .edit_segments(&mock_ctx, version, sketch_id, segments)
8741 .await
8742 .unwrap();
8743 insta::assert_snapshot!("test_edit_line_when_editing_its_start_point", src_delta.text.as_str());
8744 assert_eq!(scene_delta.new_objects, vec![]);
8745 assert_eq!(scene_delta.new_graph.objects.len(), 5);
8746
8747 ctx.close().await;
8748 mock_ctx.close().await;
8749 }
8750
8751 #[tokio::test(flavor = "multi_thread")]
8752 async fn test_edit_line_when_editing_its_end_point() {
8753 let initial_source = "\
8754sketch(on = XY) {
8755 line(start = [var 1, var 2], end = [var 3, var 4])
8756}
8757";
8758
8759 let program = Program::parse(initial_source).unwrap().0.unwrap();
8760
8761 let mut frontend = FrontendState::new();
8762
8763 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8764 let mock_ctx = ExecutorContext::new_mock(None).await;
8765 let version = Version(0);
8766
8767 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8768 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8769 let sketch_id = sketch_object.id;
8770 let sketch = expect_sketch(sketch_object);
8771 let point_id = *sketch.segments.get(1).unwrap();
8772
8773 let point_ctor = PointCtor {
8774 position: Point2d {
8775 x: Expr::Var(Number {
8776 value: 5.0,
8777 units: NumericSuffix::Inch,
8778 }),
8779 y: Expr::Var(Number {
8780 value: 6.0,
8781 units: NumericSuffix::Inch,
8782 }),
8783 },
8784 };
8785 let segments = vec![ExistingSegmentCtor {
8786 id: point_id,
8787 ctor: SegmentCtor::Point(point_ctor),
8788 }];
8789 let (src_delta, scene_delta) = frontend
8790 .edit_segments(&mock_ctx, version, sketch_id, segments)
8791 .await
8792 .unwrap();
8793 insta::assert_snapshot!("test_edit_line_when_editing_its_end_point", src_delta.text.as_str());
8794 assert_eq!(scene_delta.new_objects, vec![]);
8795 assert_eq!(
8796 scene_delta.new_graph.objects.len(),
8797 5,
8798 "{:#?}",
8799 scene_delta.new_graph.objects
8800 );
8801
8802 ctx.close().await;
8803 mock_ctx.close().await;
8804 }
8805
8806 #[tokio::test(flavor = "multi_thread")]
8807 async fn test_edit_line_with_coincident_feedback() {
8808 let initial_source = "\
8809sketch(on = XY) {
8810 line1 = line(start = [var 1, var 2], end = [var 1, var 2])
8811 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8812 fixed([line1.start, [0, 0]])
8813 coincident([line1.end, line2.start])
8814 equalLength([line1, line2])
8815}
8816";
8817
8818 let program = Program::parse(initial_source).unwrap().0.unwrap();
8819
8820 let mut frontend = FrontendState::new();
8821
8822 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8823 let mock_ctx = ExecutorContext::new_mock(None).await;
8824 let version = Version(0);
8825
8826 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8827 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8828 let sketch_id = sketch_object.id;
8829 let sketch = expect_sketch(sketch_object);
8830 let line2_end_id = *sketch.segments.get(4).unwrap();
8831
8832 let segments = vec![ExistingSegmentCtor {
8833 id: line2_end_id,
8834 ctor: SegmentCtor::Point(PointCtor {
8835 position: Point2d {
8836 x: Expr::Var(Number {
8837 value: 9.0,
8838 units: NumericSuffix::None,
8839 }),
8840 y: Expr::Var(Number {
8841 value: 10.0,
8842 units: NumericSuffix::None,
8843 }),
8844 },
8845 }),
8846 }];
8847 let (src_delta, scene_delta) = frontend
8848 .edit_segments(&mock_ctx, version, sketch_id, segments)
8849 .await
8850 .unwrap();
8851 insta::assert_snapshot!("test_edit_line_with_coincident_feedback", src_delta.text.as_str());
8852 assert_eq!(
8853 scene_delta.new_graph.objects.len(),
8854 11,
8855 "{:#?}",
8856 scene_delta.new_graph.objects
8857 );
8858
8859 ctx.close().await;
8860 mock_ctx.close().await;
8861 }
8862
8863 #[tokio::test(flavor = "multi_thread")]
8864 async fn test_edit_segments_persists_solver_feedback_for_next_mock_execute() {
8865 let initial_source = "\
8866sketch(on = XY) {
8867 line1 = line(start = [var 1, var 2], end = [var 1, var 2])
8868 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8869 fixed([line1.start, [0, 0]])
8870 coincident([line1.end, line2.start])
8871 equalLength([line1, line2])
8872}
8873";
8874
8875 let program = Program::parse(initial_source).unwrap().0.unwrap();
8876 let mut frontend = FrontendState::new();
8877 let mock_ctx = ExecutorContext::new_mock(None).await;
8878 let version = Version(0);
8879
8880 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8881 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8882 let sketch_id = sketch_object.id;
8883 let sketch = expect_sketch(sketch_object);
8884 let line2_end_id = *sketch.segments.get(4).unwrap();
8885
8886 let segments = vec![ExistingSegmentCtor {
8887 id: line2_end_id,
8888 ctor: SegmentCtor::Point(PointCtor {
8889 position: Point2d {
8890 x: Expr::Var(Number {
8891 value: 9.0,
8892 units: NumericSuffix::None,
8893 }),
8894 y: Expr::Var(Number {
8895 value: 10.0,
8896 units: NumericSuffix::None,
8897 }),
8898 },
8899 }),
8900 }];
8901 let (edited_source, _) = frontend
8902 .edit_segments(&mock_ctx, version, sketch_id, segments)
8903 .await
8904 .unwrap();
8905
8906 let (mock_source, _) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
8907 assert_eq!(mock_source.text, edited_source.text);
8908
8909 mock_ctx.close().await;
8910 }
8911
8912 #[tokio::test(flavor = "multi_thread")]
8915 async fn test_preview_edit_segments_does_not_persist_solver_feedback() {
8916 let initial_source = "\
8917sketch(on = XY) {
8918 line1 = line(start = [var 1, var 2], end = [var 1, var 2])
8919 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8920 fixed([line1.start, [0, 0]])
8921 coincident([line1.end, line2.start])
8922 equalLength([line1, line2])
8923}
8924";
8925
8926 let program = Program::parse(initial_source).unwrap().0.unwrap();
8927 let mut frontend = FrontendState::new();
8928 let mock_ctx = ExecutorContext::new_mock(None).await;
8929 let version = Version(0);
8930
8931 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8932 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8933 let sketch_id = sketch_object.id;
8934 let sketch = expect_sketch(sketch_object);
8935 let line2_end_id = *sketch.segments.get(4).unwrap();
8936
8937 let segments = vec![ExistingSegmentCtor {
8938 id: line2_end_id,
8939 ctor: SegmentCtor::Point(PointCtor {
8940 position: Point2d {
8941 x: Expr::Var(Number {
8942 value: 9.0,
8943 units: NumericSuffix::None,
8944 }),
8945 y: Expr::Var(Number {
8946 value: 10.0,
8947 units: NumericSuffix::None,
8948 }),
8949 },
8950 }),
8951 }];
8952 let (preview_source, preview_delta) = frontend
8953 .edit_segments_with_options(
8954 &mock_ctx,
8955 version,
8956 sketch_id,
8957 segments,
8958 EditSegmentsOptions {
8959 anchor_segment_ids: Some(vec![line2_end_id]),
8960 drag_anchors: Vec::new(),
8961 constraint_label_edits: Vec::new(),
8962 commit_solved_initial_guesses: false,
8963 },
8964 )
8965 .await
8966 .unwrap();
8967
8968 assert!(
8969 !preview_delta.exec_outcome.var_solutions.is_empty(),
8970 "preview solve should still solve and return geometry feedback"
8971 );
8972 assert!(
8973 preview_source
8974 .text
8975 .contains("line1 = line(start = [var 1, var 2], end = [var 1, var 2])")
8976 );
8977 assert!(
8978 preview_source
8979 .text
8980 .contains("line2 = line(start = [var 5, var 6], end = [var 9, var 10])")
8981 );
8982
8983 let (mock_source, _) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
8984 assert_eq!(mock_source.text, preview_source.text);
8985
8986 mock_ctx.close().await;
8987 }
8988
8989 #[tokio::test(flavor = "multi_thread")]
8990 async fn test_add_constraint_persists_solver_feedback_for_next_mock_execute() {
8991 let initial_source = "\
8992sketch(on = XY) {
8993 line1 = line(start = [var 0, var 0], end = [var 10, var 0])
8994}
8995";
8996
8997 let program = Program::parse(initial_source).unwrap().0.unwrap();
8998 let mut frontend = FrontendState::new();
8999 let mock_ctx = ExecutorContext::new_mock(None).await;
9000 let version = Version(0);
9001
9002 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9003 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9004 let sketch_id = sketch_object.id;
9005 let sketch = expect_sketch(sketch_object);
9006 let line_end_id = *sketch.segments.get(1).unwrap();
9007
9008 let constraint = Constraint::Fixed(Fixed {
9009 points: vec![FixedPoint {
9010 point: line_end_id,
9011 position: Point2d {
9012 x: Number {
9013 value: 20.0,
9014 units: NumericSuffix::Mm,
9015 },
9016 y: Number {
9017 value: 0.0,
9018 units: NumericSuffix::Mm,
9019 },
9020 },
9021 }],
9022 });
9023 let (constraint_source, _) = frontend
9024 .add_constraint(&mock_ctx, version, sketch_id, constraint)
9025 .await
9026 .unwrap();
9027
9028 assert!(
9029 constraint_source
9030 .text
9031 .contains("line1 = line(start = [var 0, var 0], end = [var 20, var 0])"),
9032 "{}",
9033 constraint_source.text
9034 );
9035 let (mock_source, _) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
9036 assert_eq!(mock_source.text, constraint_source.text);
9037
9038 mock_ctx.close().await;
9039 }
9040
9041 #[test]
9042 fn test_no_solver_feedback_preserves_original_source() {
9043 let initial_source = "\
9044@settings(defaultLengthUnit = in, kclVersion = 2.0)
9045cylinder = startSketchOn(XY)
9046 |> circle(center= [0, 0], radius= 22)
9047 |> extrude(length = 14)
9048";
9049 let mut frontend = FrontendState::new();
9050 frontend.program = Program::parse(initial_source).unwrap().0.unwrap();
9051 let outcome = ExecOutcome {
9052 variables: Default::default(),
9053 operations: Default::default(),
9054 artifact_graph: Default::default(),
9055 scene_objects: Default::default(),
9056 source_range_to_object: Default::default(),
9057 var_solutions: Default::default(),
9058 refactor_metadata: Default::default(),
9059 issues: Default::default(),
9060 filenames: Default::default(),
9061 default_planes: Default::default(),
9062 };
9063
9064 let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9065
9066 assert_eq!(source_delta.text, initial_source);
9067 }
9068
9069 #[tokio::test(flavor = "multi_thread")]
9072 async fn test_edit_segments_with_anchor_ids_limits_drag_fixed_constraints() {
9073 let initial_source = "\
9074sketch(on = XY) {
9075 point1 = point(at = [var 0mm, var 0mm])
9076 point2 = point(at = [var 0mm, var 0mm])
9077 coincident([point1, point2])
9078}
9079";
9080
9081 let program = Program::parse(initial_source).unwrap().0.unwrap();
9082 let mut frontend = FrontendState::new();
9083 let mock_ctx = ExecutorContext::new_mock(None).await;
9084 let version = Version(0);
9085
9086 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9087 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9088 let sketch_id = sketch_object.id;
9089 let sketch = expect_sketch(sketch_object);
9090 let point1_id = sketch.segments[0];
9091 let point2_id = sketch.segments[1];
9092
9093 let segments = vec![
9094 ExistingSegmentCtor {
9095 id: point1_id,
9096 ctor: SegmentCtor::Point(PointCtor {
9097 position: point_expr_mm(10.0, 0.0),
9098 }),
9099 },
9100 ExistingSegmentCtor {
9101 id: point2_id,
9102 ctor: SegmentCtor::Point(PointCtor {
9103 position: point_expr_mm(100.0, 0.0),
9104 }),
9105 },
9106 ];
9107 let (_, scene_delta) = frontend
9108 .edit_segments_with_options(
9109 &mock_ctx,
9110 version,
9111 sketch_id,
9112 segments,
9113 EditSegmentsOptions {
9114 anchor_segment_ids: Some(vec![point1_id]),
9115 drag_anchors: Vec::new(),
9116 constraint_label_edits: Vec::new(),
9117 commit_solved_initial_guesses: true,
9118 },
9119 )
9120 .await
9121 .unwrap();
9122
9123 assert_point_position_close(
9124 point_position(&scene_delta.new_graph, point1_id),
9125 point_number_mm(10.0, 0.0),
9126 );
9127 assert_point_position_close(
9128 point_position(&scene_delta.new_graph, point2_id),
9129 point_number_mm(10.0, 0.0),
9130 );
9131
9132 mock_ctx.close().await;
9133 }
9134
9135 fn collect_sketch_var_literals_with_value(program: &Program, value: f64) -> Vec<(SourceRange, ast::NodePath)> {
9138 use std::cell::RefCell;
9139 struct Collector {
9140 target: f64,
9141 out: RefCell<Vec<(SourceRange, ast::NodePath)>>,
9142 }
9143 impl<'a> crate::walk::Visitor<'a> for &Collector {
9144 type Error = crate::front::Error;
9145 fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
9146 if let crate::walk::Node::SketchVar(sketch_var) = node
9147 && let (Some(initial), Some(node_path)) = (&sketch_var.initial, &sketch_var.node_path)
9148 && (initial.value - self.target).abs() < 1e-9
9149 {
9150 self.out
9151 .borrow_mut()
9152 .push((SourceRange::from(initial.as_ref()), node_path.clone()));
9153 }
9154 for child in node.children().iter() {
9155 if !child.visit(*self)? {
9156 return Ok(false);
9157 }
9158 }
9159 Ok(true)
9160 }
9161 }
9162 let collector = Collector {
9163 target: value,
9164 out: Default::default(),
9165 };
9166 let _ = crate::walk::Node::from(&program.ast).visit(&collector);
9167 collector.out.into_inner()
9168 }
9169
9170 fn collect_all_sketch_vars(program: &Program) -> Vec<(SourceRange, ast::NodePath)> {
9173 use std::cell::RefCell;
9174 struct Collector {
9175 out: RefCell<Vec<(SourceRange, ast::NodePath)>>,
9176 }
9177 impl<'a> crate::walk::Visitor<'a> for &Collector {
9178 type Error = crate::front::Error;
9179 fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
9180 if let crate::walk::Node::SketchVar(sketch_var) = node
9181 && let Some(node_path) = &sketch_var.node_path
9182 {
9183 self.out
9184 .borrow_mut()
9185 .push((SourceRange::from(sketch_var), node_path.clone()));
9186 }
9187 for child in node.children().iter() {
9188 if !child.visit(*self)? {
9189 return Ok(false);
9190 }
9191 }
9192 Ok(true)
9193 }
9194 }
9195 let collector = Collector {
9196 out: Default::default(),
9197 };
9198 let _ = crate::walk::Node::from(&program.ast).visit(&collector);
9199 collector.out.into_inner()
9200 }
9201
9202 fn empty_exec_outcome_with_var_solutions(
9203 var_solutions: Vec<(SourceRange, Option<ast::NodePath>, Number)>,
9204 ) -> ExecOutcome {
9205 ExecOutcome {
9206 variables: Default::default(),
9207 operations: Default::default(),
9208 artifact_graph: Default::default(),
9209 scene_objects: Default::default(),
9210 source_range_to_object: Default::default(),
9211 var_solutions,
9212 refactor_metadata: Default::default(),
9213 issues: Default::default(),
9214 filenames: Default::default(),
9215 default_planes: Default::default(),
9216 }
9217 }
9218
9219 #[test]
9223 fn test_commit_var_solution_by_node_path_updates_sketch_var() {
9224 let initial_source = "\
9225sketch(on = XY) {
9226 line1 = line(start = [var 0, var 0], end = [var 10mm, var 0])
9227}
9228";
9229 let program = Program::parse(initial_source).unwrap().0.unwrap();
9230 let matches = collect_sketch_var_literals_with_value(&program, 10.0);
9231 assert_eq!(matches.len(), 1, "expected exactly one `var 10mm`");
9232 let (literal_range, node_path) = matches.into_iter().next().unwrap();
9233
9234 let mut frontend = FrontendState::new();
9235 frontend.program = program;
9236
9237 let outcome = empty_exec_outcome_with_var_solutions(vec![(
9238 literal_range,
9239 Some(node_path),
9240 Number {
9241 value: 25.0,
9242 units: NumericSuffix::Mm,
9243 },
9244 )]);
9245
9246 let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9247
9248 insta::assert_snapshot!(
9249 "test_commit_var_solution_by_node_path_updates_sketch_var",
9250 source_delta.text
9251 );
9252 }
9253
9254 #[test]
9261 fn test_commit_var_solution_survives_whitespace_shift_earlier_in_file() {
9262 let compact_source = "\
9263sketch(on = XY) {
9264 line1 = line(start = [var 0, var 0], end = [var 10mm, var 0])
9265}
9266";
9267 let padded_source = "\
9268// added comment\n// added comment\n\nsketch(on = XY) {
9269 line1 = line(start = [var 0, var 0], end = [var 10mm, var 0])
9270}
9271";
9272 let compact_program = Program::parse(compact_source).unwrap().0.unwrap();
9273 let padded_program = Program::parse(padded_source).unwrap().0.unwrap();
9274
9275 let compact_match = collect_sketch_var_literals_with_value(&compact_program, 10.0)
9276 .into_iter()
9277 .next()
9278 .expect("expected `var 10mm` in compact source");
9279 let padded_match = collect_sketch_var_literals_with_value(&padded_program, 10.0)
9280 .into_iter()
9281 .next()
9282 .expect("expected `var 10mm` in padded source");
9283
9284 assert_ne!(
9285 compact_match.0, padded_match.0,
9286 "byte offsets must differ for this test to be meaningful"
9287 );
9288 assert_eq!(
9289 compact_match.1, padded_match.1,
9290 "node paths must agree across whitespace; that's the whole point of NodePath",
9291 );
9292
9293 let mut frontend = FrontendState::new();
9294 frontend.program = padded_program;
9295
9296 let outcome = empty_exec_outcome_with_var_solutions(vec![(
9298 compact_match.0,
9299 Some(compact_match.1),
9300 Number {
9301 value: 30.0,
9302 units: NumericSuffix::Mm,
9303 },
9304 )]);
9305
9306 let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9307
9308 insta::assert_snapshot!(
9309 "test_commit_var_solution_survives_whitespace_shift_earlier_in_file",
9310 source_delta.text
9311 );
9312 }
9313
9314 #[test]
9318 fn test_commit_var_solution_node_path_wins_when_source_range_points_at_wrong_var() {
9319 let initial_source = "\
9320sketch(on = XY) {
9321 line1 = line(start = [var 10mm, var 0mm], end = [var 20mm, var 0mm])
9322}
9323";
9324 let program = Program::parse(initial_source).unwrap().0.unwrap();
9325
9326 let var_10 = collect_sketch_var_literals_with_value(&program, 10.0)
9327 .into_iter()
9328 .next()
9329 .expect("expected `var 10mm`");
9330 let var_20 = collect_sketch_var_literals_with_value(&program, 20.0)
9331 .into_iter()
9332 .next()
9333 .expect("expected `var 20mm`");
9334
9335 let mut frontend = FrontendState::new();
9336 frontend.program = program;
9337
9338 let outcome = empty_exec_outcome_with_var_solutions(vec![(
9340 var_20.0,
9341 Some(var_10.1),
9342 Number {
9343 value: 33.0,
9344 units: NumericSuffix::Mm,
9345 },
9346 )]);
9347
9348 let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9349
9350 insta::assert_snapshot!(
9351 "test_commit_var_solution_node_path_wins_when_source_range_points_at_wrong_var",
9352 source_delta.text
9353 );
9354 }
9355
9356 #[test]
9363 fn test_commit_var_solution_writes_back_into_bare_var() {
9364 let initial_source = "\
9365@settings(experimentalFeatures = allow, kclVersion = 2.0)
9366sketch(on = XY) {
9367 line1 = line(start = [var, var 0mm], end = [var 10mm, var 0])
9368}
9369";
9370 let program = Program::parse(initial_source).unwrap().0.unwrap();
9371
9372 let bare = collect_all_sketch_vars(&program)
9375 .into_iter()
9376 .find(|(range, _)| {
9377 range.end() - range.start() == 3
9379 })
9380 .expect("expected at least one bare `var`");
9381
9382 let mut frontend = FrontendState::new();
9383 frontend.program = program;
9384
9385 let outcome = empty_exec_outcome_with_var_solutions(vec![(
9386 bare.0,
9387 Some(bare.1),
9388 Number {
9389 value: 7.0,
9390 units: NumericSuffix::Mm,
9391 },
9392 )]);
9393
9394 let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9395
9396 insta::assert_snapshot!("test_commit_var_solution_writes_back_into_bare_var", source_delta.text);
9400 }
9401
9402 #[tokio::test(flavor = "multi_thread")]
9403 async fn test_delete_point_without_var() {
9404 let initial_source = "\
9405sketch(on = XY) {
9406 point(at = [var 1, var 2])
9407 point(at = [var 3, var 4])
9408 point(at = [var 5, var 6])
9409}
9410";
9411
9412 let program = Program::parse(initial_source).unwrap().0.unwrap();
9413
9414 let mut frontend = FrontendState::new();
9415
9416 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9417 let mock_ctx = ExecutorContext::new_mock(None).await;
9418 let version = Version(0);
9419
9420 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9421 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9422 let sketch_id = sketch_object.id;
9423 let sketch = expect_sketch(sketch_object);
9424
9425 let point_id = *sketch.segments.get(1).unwrap();
9426
9427 let (src_delta, scene_delta) = frontend
9428 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
9429 .await
9430 .unwrap();
9431 insta::assert_snapshot!("test_delete_point_without_var", src_delta.text.as_str());
9432 assert_eq!(scene_delta.new_objects, vec![]);
9433 assert_eq!(scene_delta.new_graph.objects.len(), 4);
9434
9435 ctx.close().await;
9436 mock_ctx.close().await;
9437 }
9438
9439 #[tokio::test(flavor = "multi_thread")]
9440 async fn test_delete_point_with_var() {
9441 let initial_source = "\
9442sketch(on = XY) {
9443 point(at = [var 1, var 2])
9444 point1 = point(at = [var 3, var 4])
9445 point(at = [var 5, var 6])
9446}
9447";
9448
9449 let program = Program::parse(initial_source).unwrap().0.unwrap();
9450
9451 let mut frontend = FrontendState::new();
9452
9453 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9454 let mock_ctx = ExecutorContext::new_mock(None).await;
9455 let version = Version(0);
9456
9457 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9458 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9459 let sketch_id = sketch_object.id;
9460 let sketch = expect_sketch(sketch_object);
9461
9462 let point_id = *sketch.segments.get(1).unwrap();
9463
9464 let (src_delta, scene_delta) = frontend
9465 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
9466 .await
9467 .unwrap();
9468 insta::assert_snapshot!("test_delete_point_with_var", src_delta.text.as_str());
9469 assert_eq!(scene_delta.new_objects, vec![]);
9470 assert_eq!(scene_delta.new_graph.objects.len(), 4);
9471
9472 ctx.close().await;
9473 mock_ctx.close().await;
9474 }
9475
9476 #[tokio::test(flavor = "multi_thread")]
9477 async fn test_delete_multiple_points() {
9478 let initial_source = "\
9479sketch(on = XY) {
9480 point(at = [var 1, var 2])
9481 point1 = point(at = [var 3, var 4])
9482 point(at = [var 5, var 6])
9483}
9484";
9485
9486 let program = Program::parse(initial_source).unwrap().0.unwrap();
9487
9488 let mut frontend = FrontendState::new();
9489
9490 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9491 let mock_ctx = ExecutorContext::new_mock(None).await;
9492 let version = Version(0);
9493
9494 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9495 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9496 let sketch_id = sketch_object.id;
9497
9498 let sketch = expect_sketch(sketch_object);
9499
9500 let point1_id = *sketch.segments.first().unwrap();
9501 let point2_id = *sketch.segments.get(1).unwrap();
9502
9503 let (src_delta, scene_delta) = frontend
9504 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point1_id, point2_id])
9505 .await
9506 .unwrap();
9507 insta::assert_snapshot!("test_delete_multiple_points", src_delta.text.as_str());
9508 assert_eq!(scene_delta.new_objects, vec![]);
9509 assert_eq!(scene_delta.new_graph.objects.len(), 3);
9510
9511 ctx.close().await;
9512 mock_ctx.close().await;
9513 }
9514
9515 #[tokio::test(flavor = "multi_thread")]
9516 async fn test_delete_coincident_constraint() {
9517 let initial_source = "\
9518sketch(on = XY) {
9519 point1 = point(at = [var 1, var 2])
9520 point2 = point(at = [var 3, var 4])
9521 coincident([point1, point2])
9522 point(at = [var 5, var 6])
9523}
9524";
9525
9526 let program = Program::parse(initial_source).unwrap().0.unwrap();
9527
9528 let mut frontend = FrontendState::new();
9529
9530 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9531 let mock_ctx = ExecutorContext::new_mock(None).await;
9532 let version = Version(0);
9533
9534 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9535 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9536 let sketch_id = sketch_object.id;
9537 let sketch = expect_sketch(sketch_object);
9538
9539 let coincident_id = *sketch.constraints.first().unwrap();
9540
9541 let (src_delta, scene_delta) = frontend
9542 .delete_objects(&mock_ctx, version, sketch_id, vec![coincident_id], Vec::new())
9543 .await
9544 .unwrap();
9545 insta::assert_snapshot!("test_delete_coincident_constraint", src_delta.text.as_str());
9546 assert_eq!(scene_delta.new_objects, vec![]);
9547 assert_eq!(scene_delta.new_graph.objects.len(), 5);
9548
9549 ctx.close().await;
9550 mock_ctx.close().await;
9551 }
9552
9553 #[tokio::test(flavor = "multi_thread")]
9554 async fn test_delete_line_cascades_to_coincident_constraint() {
9555 let initial_source = "\
9556sketch(on = XY) {
9557 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9558 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9559 coincident([line1.end, line2.start])
9560}
9561";
9562
9563 let program = Program::parse(initial_source).unwrap().0.unwrap();
9564
9565 let mut frontend = FrontendState::new();
9566
9567 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9568 let mock_ctx = ExecutorContext::new_mock(None).await;
9569 let version = Version(0);
9570
9571 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9572 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9573 let sketch_id = sketch_object.id;
9574 let sketch = expect_sketch(sketch_object);
9575 let line_id = *sketch.segments.get(5).unwrap();
9576
9577 let (src_delta, scene_delta) = frontend
9578 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
9579 .await
9580 .unwrap();
9581 insta::assert_snapshot!(
9582 "test_delete_line_cascades_to_coincident_constraint",
9583 src_delta.text.as_str()
9584 );
9585 assert_eq!(
9586 scene_delta.new_graph.objects.len(),
9587 5,
9588 "{:#?}",
9589 scene_delta.new_graph.objects
9590 );
9591
9592 ctx.close().await;
9593 mock_ctx.close().await;
9594 }
9595
9596 #[tokio::test(flavor = "multi_thread")]
9597 async fn test_delete_line_cascades_to_distance_constraint() {
9598 let initial_source = "\
9599sketch(on = XY) {
9600 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9601 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9602 distance([line1.end, line2.start]) == 10mm
9603}
9604";
9605
9606 let program = Program::parse(initial_source).unwrap().0.unwrap();
9607
9608 let mut frontend = FrontendState::new();
9609
9610 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9611 let mock_ctx = ExecutorContext::new_mock(None).await;
9612 let version = Version(0);
9613
9614 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9615 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9616 let sketch_id = sketch_object.id;
9617 let sketch = expect_sketch(sketch_object);
9618 let line_id = *sketch.segments.get(5).unwrap();
9619
9620 let (src_delta, scene_delta) = frontend
9621 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
9622 .await
9623 .unwrap();
9624 insta::assert_snapshot!(
9625 "test_delete_line_cascades_to_distance_constraint",
9626 src_delta.text.as_str()
9627 );
9628 assert_eq!(
9629 scene_delta.new_graph.objects.len(),
9630 5,
9631 "{:#?}",
9632 scene_delta.new_graph.objects
9633 );
9634
9635 ctx.close().await;
9636 mock_ctx.close().await;
9637 }
9638
9639 #[tokio::test(flavor = "multi_thread")]
9640 async fn test_delete_point_cascades_to_horizontal_distance_constraint() {
9641 let initial_source = "\
9642sketch(on = XY) {
9643 point1 = point(at = [var 1, var 2])
9644 point2 = point(at = [var 3, var 4])
9645 horizontalDistance([point1, point2]) == 10mm
9646}
9647";
9648
9649 let program = Program::parse(initial_source).unwrap().0.unwrap();
9650
9651 let mut frontend = FrontendState::new();
9652
9653 let mock_ctx = ExecutorContext::new_mock(None).await;
9654 let version = Version(0);
9655
9656 frontend.program = program.clone();
9657 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9658 frontend.update_state_after_exec(outcome, true);
9659 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9660 let sketch_id = sketch_object.id;
9661 let sketch = expect_sketch(sketch_object);
9662 let point2_id = *sketch.segments.get(1).unwrap();
9663
9664 let (src_delta, scene_delta) = frontend
9665 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point2_id])
9666 .await
9667 .unwrap();
9668 insta::assert_snapshot!(
9669 "test_delete_point_cascades_to_horizontal_distance_constraint",
9670 src_delta.text.as_str()
9671 );
9672 assert_eq!(
9673 scene_delta.new_graph.objects.len(),
9674 3,
9675 "{:#?}",
9676 scene_delta.new_graph.objects
9677 );
9678
9679 mock_ctx.close().await;
9680 }
9681
9682 #[tokio::test(flavor = "multi_thread")]
9683 async fn test_delete_line_cascades_to_fixed_constraint() {
9684 let initial_source = "\
9685sketch(on = XY) {
9686 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9687 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9688 fixed([line1.start, [0, 0]])
9689}
9690";
9691
9692 let program = Program::parse(initial_source).unwrap().0.unwrap();
9693
9694 let mut frontend = FrontendState::new();
9695
9696 let mock_ctx = ExecutorContext::new_mock(None).await;
9697 let version = Version(0);
9698
9699 frontend.program = program.clone();
9700 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9701 frontend.update_state_after_exec(outcome, true);
9702 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9703 let sketch_id = sketch_object.id;
9704 let sketch = expect_sketch(sketch_object);
9705 let line1_id = *sketch.segments.get(2).unwrap();
9706
9707 let (src_delta, scene_delta) = frontend
9708 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
9709 .await
9710 .unwrap();
9711 insta::assert_snapshot!("test_delete_line_cascades_to_fixed_constraint", src_delta.text.as_str());
9712 assert_eq!(
9713 scene_delta.new_graph.objects.len(),
9714 5,
9715 "{:#?}",
9716 scene_delta.new_graph.objects
9717 );
9718
9719 mock_ctx.close().await;
9720 }
9721
9722 #[tokio::test(flavor = "multi_thread")]
9723 async fn test_delete_line_cascades_to_midpoint_constraint() {
9724 let initial_source = "\
9725sketch(on = XY) {
9726 point1 = point(at = [var 1, var 2])
9727 line1 = line(start = [var 0, var 0], end = [var 6, var 4])
9728 midpoint(line1, point = point1)
9729}
9730";
9731
9732 let program = Program::parse(initial_source).unwrap().0.unwrap();
9733
9734 let mut frontend = FrontendState::new();
9735
9736 let mock_ctx = ExecutorContext::new_mock(None).await;
9737 let version = Version(0);
9738
9739 frontend.program = program.clone();
9740 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9741 frontend.update_state_after_exec(outcome, true);
9742 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9743 let sketch_id = sketch_object.id;
9744 let sketch = expect_sketch(sketch_object);
9745 let line1_id = *sketch.segments.get(3).unwrap();
9746
9747 let (src_delta, scene_delta) = frontend
9748 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
9749 .await
9750 .unwrap();
9751 insta::assert_snapshot!(
9752 "test_delete_line_cascades_to_midpoint_constraint",
9753 src_delta.text.as_str()
9754 );
9755 assert_eq!(
9756 scene_delta.new_graph.objects.len(),
9757 3,
9758 "{:#?}",
9759 scene_delta.new_graph.objects
9760 );
9761
9762 mock_ctx.close().await;
9763 }
9764
9765 #[tokio::test(flavor = "multi_thread")]
9766 async fn test_delete_point_preserves_multiline_coincident_constraint() {
9767 let initial_source = "\
9768sketch(on = XY) {
9769 point1 = point(at = [var 1, var 2])
9770 point2 = point(at = [var 3, var 4])
9771 point3 = point(at = [var 5, var 6])
9772 coincident([point1, point2, point3])
9773}
9774";
9775
9776 let program = Program::parse(initial_source).unwrap().0.unwrap();
9777
9778 let mut frontend = FrontendState::new();
9779
9780 let mock_ctx = ExecutorContext::new_mock(None).await;
9781 let version = Version(0);
9782
9783 frontend.program = program.clone();
9784 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9785 frontend.update_state_after_exec(outcome, true);
9786 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9787 let sketch_id = sketch_object.id;
9788 let sketch = expect_sketch(sketch_object);
9789 let point3_id = *sketch.segments.get(2).unwrap();
9790
9791 let (src_delta, scene_delta) = frontend
9792 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point3_id])
9793 .await
9794 .unwrap();
9795 assert!(src_delta.text.contains("point1 = point("), "{}", src_delta.text);
9796 assert!(src_delta.text.contains("point2 = point("), "{}", src_delta.text);
9797 assert!(!src_delta.text.contains("point3 = point("), "{}", src_delta.text);
9798 assert!(
9799 src_delta.text.contains("coincident([point1, point2])"),
9800 "{}",
9801 src_delta.text
9802 );
9803
9804 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9805 let sketch = expect_sketch(sketch_object);
9806 assert_eq!(sketch.segments.len(), 2);
9807 assert_eq!(sketch.constraints.len(), 1);
9808
9809 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
9810 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9811 panic!("Expected constraint object");
9812 };
9813 let Constraint::Coincident(coincident) = constraint else {
9814 panic!("Expected coincident constraint");
9815 };
9816 assert_eq!(
9817 coincident.segments,
9818 sketch
9819 .segments
9820 .iter()
9821 .copied()
9822 .map(Into::into)
9823 .collect::<Vec<ConstraintSegment>>()
9824 );
9825
9826 mock_ctx.close().await;
9827 }
9828
9829 #[tokio::test(flavor = "multi_thread")]
9830 async fn test_delete_line_preserves_multiline_equal_length_constraint() {
9831 let initial_source = "\
9832sketch(on = XY) {
9833 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9834 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9835 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
9836 equalLength([line1, line2, line3])
9837}
9838";
9839
9840 let program = Program::parse(initial_source).unwrap().0.unwrap();
9841
9842 let mut frontend = FrontendState::new();
9843
9844 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9845 let mock_ctx = ExecutorContext::new_mock(None).await;
9846 let version = Version(0);
9847
9848 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9849 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9850 let sketch_id = sketch_object.id;
9851 let sketch = expect_sketch(sketch_object);
9852 let line3_id = *sketch.segments.get(8).unwrap();
9853
9854 let (src_delta, scene_delta) = frontend
9855 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line3_id])
9856 .await
9857 .unwrap();
9858 insta::assert_snapshot!(
9859 "test_delete_line_preserves_multiline_equal_length_constraint",
9860 src_delta.text.as_str()
9861 );
9862
9863 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9864 let sketch = expect_sketch(sketch_object);
9865 assert_eq!(sketch.constraints.len(), 1);
9866
9867 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
9868 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9869 panic!("Expected constraint object");
9870 };
9871 let Constraint::LinesEqualLength(lines_equal_length) = constraint else {
9872 panic!("Expected lines equal length constraint");
9873 };
9874 assert_eq!(lines_equal_length.lines.len(), 2);
9875
9876 ctx.close().await;
9877 mock_ctx.close().await;
9878 }
9879
9880 #[tokio::test(flavor = "multi_thread")]
9881 async fn test_delete_line_preserves_multiline_horizontal_constraint() {
9882 let initial_source = "\
9883sketch(on = XY) {
9884 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9885 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9886 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
9887 horizontal([line1.end, line2.start, line3.start])
9888}
9889";
9890
9891 let program = Program::parse(initial_source).unwrap().0.unwrap();
9892
9893 let mut frontend = FrontendState::new();
9894
9895 let mock_ctx = ExecutorContext::new_mock(None).await;
9896 let version = Version(0);
9897
9898 frontend.program = program.clone();
9899 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9900 frontend.update_state_after_exec(outcome, true);
9901 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9902 let sketch_id = sketch_object.id;
9903 let sketch = expect_sketch(sketch_object);
9904 let line1_id = *sketch.segments.get(2).unwrap();
9905
9906 let (src_delta, scene_delta) = frontend
9907 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
9908 .await
9909 .unwrap();
9910 assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
9911 assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
9912 assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
9913 assert!(
9914 src_delta.text.contains("horizontal([line2.start, line3.start])"),
9915 "{}",
9916 src_delta.text
9917 );
9918
9919 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9920 let sketch = expect_sketch(sketch_object);
9921 assert_eq!(sketch.constraints.len(), 1);
9922
9923 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
9924 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9925 panic!("Expected constraint object");
9926 };
9927 let Constraint::Horizontal(Horizontal::Points { points }) = constraint else {
9928 panic!("Expected horizontal points constraint");
9929 };
9930 let remaining_points = vec![sketch.segments[0].into(), sketch.segments[3].into()];
9931 assert_eq!(*points, remaining_points);
9932
9933 mock_ctx.close().await;
9934 }
9935
9936 #[tokio::test(flavor = "multi_thread")]
9937 async fn test_delete_line_preserves_multiline_vertical_constraint() {
9938 let initial_source = "\
9939sketch(on = XY) {
9940 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9941 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9942 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
9943 vertical([line1.end, line2.start, line3.start])
9944}
9945";
9946
9947 let program = Program::parse(initial_source).unwrap().0.unwrap();
9948
9949 let mut frontend = FrontendState::new();
9950
9951 let mock_ctx = ExecutorContext::new_mock(None).await;
9952 let version = Version(0);
9953
9954 frontend.program = program.clone();
9955 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9956 frontend.update_state_after_exec(outcome, true);
9957 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9958 let sketch_id = sketch_object.id;
9959 let sketch = expect_sketch(sketch_object);
9960 let line1_id = *sketch.segments.get(2).unwrap();
9961
9962 let (src_delta, scene_delta) = frontend
9963 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
9964 .await
9965 .unwrap();
9966 assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
9967 assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
9968 assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
9969 assert!(
9970 src_delta.text.contains("vertical([line2.start, line3.start])"),
9971 "{}",
9972 src_delta.text
9973 );
9974
9975 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9976 let sketch = expect_sketch(sketch_object);
9977 assert_eq!(sketch.constraints.len(), 1);
9978
9979 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
9980 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9981 panic!("Expected constraint object");
9982 };
9983 let Constraint::Vertical(Vertical::Points { points }) = constraint else {
9984 panic!("Expected vertical points constraint");
9985 };
9986 let remaining_points = vec![sketch.segments[0].into(), sketch.segments[3].into()];
9987 assert_eq!(*points, remaining_points);
9988
9989 mock_ctx.close().await;
9990 }
9991
9992 #[tokio::test(flavor = "multi_thread")]
9993 async fn test_delete_line_preserves_multiline_coincident_constraint() {
9994 let initial_source = "\
9995sketch(on = XY) {
9996 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9997 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9998 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
9999 coincident([line1.end, line2.start, line3.start])
10000}
10001";
10002
10003 let program = Program::parse(initial_source).unwrap().0.unwrap();
10004
10005 let mut frontend = FrontendState::new();
10006
10007 let mock_ctx = ExecutorContext::new_mock(None).await;
10008 let version = Version(0);
10009
10010 frontend.program = program.clone();
10011 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10012 frontend.update_state_after_exec(outcome, true);
10013 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10014 let sketch_id = sketch_object.id;
10015 let sketch = expect_sketch(sketch_object);
10016 let line1_id = *sketch.segments.get(2).unwrap();
10017
10018 let (src_delta, scene_delta) = frontend
10019 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
10020 .await
10021 .unwrap();
10022 assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
10023 assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
10024 assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
10025 assert!(
10026 src_delta.text.contains("coincident([line2.start, line3.start])"),
10027 "{}",
10028 src_delta.text
10029 );
10030
10031 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10032 let sketch = expect_sketch(sketch_object);
10033 assert_eq!(sketch.constraints.len(), 1);
10034
10035 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10036 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10037 panic!("Expected constraint object");
10038 };
10039 let Constraint::Coincident(coincident) = constraint else {
10040 panic!("Expected coincident constraint");
10041 };
10042 let remaining_segments = vec![sketch.segments[0].into(), sketch.segments[3].into()];
10043 assert_eq!(coincident.segments, remaining_segments);
10044
10045 mock_ctx.close().await;
10046 }
10047
10048 #[tokio::test(flavor = "multi_thread")]
10049 async fn test_delete_lines_removes_multiline_equal_length_constraint_below_minimum() {
10050 let initial_source = "\
10051sketch(on = XY) {
10052 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10053 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10054 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10055 equalLength([line1, line2, line3])
10056}
10057";
10058
10059 let program = Program::parse(initial_source).unwrap().0.unwrap();
10060
10061 let mut frontend = FrontendState::new();
10062
10063 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10064 let mock_ctx = ExecutorContext::new_mock(None).await;
10065 let version = Version(0);
10066
10067 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10068 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10069 let sketch_id = sketch_object.id;
10070 let sketch = expect_sketch(sketch_object);
10071 let line2_id = *sketch.segments.get(5).unwrap();
10072 let line3_id = *sketch.segments.get(8).unwrap();
10073
10074 let (src_delta, scene_delta) = frontend
10075 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line2_id, line3_id])
10076 .await
10077 .unwrap();
10078 insta::assert_snapshot!(
10079 "test_delete_lines_removes_multiline_equal_length_constraint_below_minimum",
10080 src_delta.text.as_str()
10081 );
10082
10083 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10084 let sketch = expect_sketch(sketch_object);
10085 assert!(sketch.constraints.is_empty());
10086
10087 ctx.close().await;
10088 mock_ctx.close().await;
10089 }
10090
10091 #[tokio::test(flavor = "multi_thread")]
10092 async fn test_delete_line_preserves_multiline_parallel_constraint() {
10093 let initial_source = "\
10094sketch(on = XY) {
10095 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10096 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10097 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10098 parallel([line1, line2, line3])
10099}
10100";
10101
10102 let program = Program::parse(initial_source).unwrap().0.unwrap();
10103
10104 let mut frontend = FrontendState::new();
10105
10106 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10107 let mock_ctx = ExecutorContext::new_mock(None).await;
10108 let version = Version(0);
10109
10110 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10111 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10112 let sketch_id = sketch_object.id;
10113 let sketch = expect_sketch(sketch_object);
10114 let line3_id = *sketch.segments.get(8).unwrap();
10115
10116 let (src_delta, scene_delta) = frontend
10117 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line3_id])
10118 .await
10119 .unwrap();
10120 insta::assert_snapshot!(
10121 "test_delete_line_preserves_multiline_parallel_constraint",
10122 src_delta.text.as_str()
10123 );
10124
10125 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10126 let sketch = expect_sketch(sketch_object);
10127 assert_eq!(sketch.constraints.len(), 1);
10128
10129 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10130 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10131 panic!("Expected constraint object");
10132 };
10133 let Constraint::Parallel(parallel) = constraint else {
10134 panic!("Expected parallel constraint");
10135 };
10136 assert_eq!(parallel.lines.len(), 2);
10137
10138 ctx.close().await;
10139 mock_ctx.close().await;
10140 }
10141
10142 #[tokio::test(flavor = "multi_thread")]
10143 async fn test_delete_lines_removes_multiline_parallel_constraint_below_minimum() {
10144 let initial_source = "\
10145sketch(on = XY) {
10146 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10147 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10148 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10149 parallel([line1, line2, line3])
10150}
10151";
10152
10153 let program = Program::parse(initial_source).unwrap().0.unwrap();
10154
10155 let mut frontend = FrontendState::new();
10156
10157 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10158 let mock_ctx = ExecutorContext::new_mock(None).await;
10159 let version = Version(0);
10160
10161 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10162 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10163 let sketch_id = sketch_object.id;
10164 let sketch = expect_sketch(sketch_object);
10165 let line2_id = *sketch.segments.get(5).unwrap();
10166 let line3_id = *sketch.segments.get(8).unwrap();
10167
10168 let (src_delta, scene_delta) = frontend
10169 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line2_id, line3_id])
10170 .await
10171 .unwrap();
10172 insta::assert_snapshot!(
10173 "test_delete_lines_removes_multiline_parallel_constraint_below_minimum",
10174 src_delta.text.as_str()
10175 );
10176
10177 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10178 let sketch = expect_sketch(sketch_object);
10179 assert!(sketch.constraints.is_empty());
10180
10181 ctx.close().await;
10182 mock_ctx.close().await;
10183 }
10184
10185 #[tokio::test(flavor = "multi_thread")]
10186 async fn test_delete_line_line_coincident_constraint() {
10187 let initial_source = "\
10188sketch(on = XY) {
10189 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10190 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10191 coincident([line1, line2])
10192}
10193";
10194
10195 let program = Program::parse(initial_source).unwrap().0.unwrap();
10196
10197 let mut frontend = FrontendState::new();
10198
10199 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10200 let mock_ctx = ExecutorContext::new_mock(None).await;
10201 let version = Version(0);
10202
10203 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10204 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10205 let sketch_id = sketch_object.id;
10206 let sketch = expect_sketch(sketch_object);
10207
10208 let coincident_id = *sketch.constraints.first().unwrap();
10209
10210 let (src_delta, scene_delta) = frontend
10211 .delete_objects(&mock_ctx, version, sketch_id, vec![coincident_id], Vec::new())
10212 .await
10213 .unwrap();
10214 insta::assert_snapshot!("test_delete_line_line_coincident_constraint", src_delta.text.as_str());
10215 assert_eq!(scene_delta.new_objects, vec![]);
10216 assert_eq!(scene_delta.new_graph.objects.len(), 8);
10217
10218 ctx.close().await;
10219 mock_ctx.close().await;
10220 }
10221
10222 #[tokio::test(flavor = "multi_thread")]
10223 async fn test_two_points_coincident() {
10224 let initial_source = "\
10225sketch(on = XY) {
10226 point1 = point(at = [var 1, var 2])
10227 point(at = [3, 4])
10228}
10229";
10230
10231 let program = Program::parse(initial_source).unwrap().0.unwrap();
10232
10233 let mut frontend = FrontendState::new();
10234
10235 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10236 let mock_ctx = ExecutorContext::new_mock(None).await;
10237 let version = Version(0);
10238
10239 frontend.hack_set_program(&ctx, program).await.unwrap();
10240 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10241 let sketch_id = sketch_object.id;
10242 let sketch = expect_sketch(sketch_object);
10243 let point0_id = *sketch.segments.first().unwrap();
10244 let point1_id = *sketch.segments.get(1).unwrap();
10245
10246 let constraint = Constraint::Coincident(Coincident {
10247 segments: vec![point0_id.into(), point1_id.into()],
10248 });
10249 let (src_delta, scene_delta) = frontend
10250 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10251 .await
10252 .unwrap();
10253 insta::assert_snapshot!("test_two_points_coincident", src_delta.text.as_str());
10254 assert_eq!(
10255 scene_delta.new_graph.objects.len(),
10256 5,
10257 "{:#?}",
10258 scene_delta.new_graph.objects
10259 );
10260
10261 ctx.close().await;
10262 mock_ctx.close().await;
10263 }
10264
10265 #[tokio::test(flavor = "multi_thread")]
10266 async fn test_three_points_coincident() {
10267 let initial_source = "\
10268sketch(on = XY) {
10269 point1 = point(at = [var 1, var 2])
10270 point(at = [var 3, var 4])
10271 point(at = [var 5, var 6])
10272}
10273";
10274
10275 let program = Program::parse(initial_source).unwrap().0.unwrap();
10276
10277 let mut frontend = FrontendState::new();
10278
10279 let mock_ctx = ExecutorContext::new_mock(None).await;
10280 let version = Version(0);
10281
10282 frontend.program = program.clone();
10283 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10284 frontend.update_state_after_exec(outcome, true);
10285 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10286 let sketch_id = sketch_object.id;
10287 let sketch = expect_sketch(sketch_object);
10288 let segments = sketch
10289 .segments
10290 .iter()
10291 .take(3)
10292 .copied()
10293 .map(Into::into)
10294 .collect::<Vec<ConstraintSegment>>();
10295
10296 let constraint = Constraint::Coincident(Coincident {
10297 segments: segments.clone(),
10298 });
10299 let (src_delta, scene_delta) = frontend
10300 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10301 .await
10302 .unwrap();
10303 insta::assert_snapshot!("test_three_points_coincident", src_delta.text.as_str());
10304
10305 let constraint_object = scene_delta
10306 .new_graph
10307 .objects
10308 .iter()
10309 .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
10310 .unwrap();
10311
10312 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10313 panic!("expected a constraint object");
10314 };
10315
10316 assert_eq!(constraint, &Constraint::Coincident(Coincident { segments }));
10317
10318 mock_ctx.close().await;
10319 }
10320
10321 #[tokio::test(flavor = "multi_thread")]
10322 async fn test_source_with_three_point_coincident_tracks_all_segments() {
10323 let initial_source = "\
10324sketch(on = XY) {
10325 point1 = point(at = [var 1, var 2])
10326 point2 = point(at = [var 3, var 4])
10327 point3 = point(at = [var 5, var 6])
10328 coincident([point1, point2, point3])
10329}
10330";
10331
10332 let program = Program::parse(initial_source).unwrap().0.unwrap();
10333
10334 let mut frontend = FrontendState::new();
10335
10336 let ctx = ExecutorContext::new_mock(None).await;
10337 frontend.program = program.clone();
10338 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10339 frontend.update_state_after_exec(outcome, true);
10340
10341 let constraint_object = frontend
10342 .scene_graph
10343 .objects
10344 .iter()
10345 .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
10346 .unwrap();
10347 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10348 panic!("expected a constraint object");
10349 };
10350
10351 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10352 let sketch = expect_sketch(sketch_object);
10353 let expected_segments = sketch
10354 .segments
10355 .iter()
10356 .take(3)
10357 .copied()
10358 .map(Into::into)
10359 .collect::<Vec<ConstraintSegment>>();
10360
10361 assert_eq!(
10362 constraint,
10363 &Constraint::Coincident(Coincident {
10364 segments: expected_segments,
10365 })
10366 );
10367
10368 ctx.close().await;
10369 }
10370
10371 #[tokio::test(flavor = "multi_thread")]
10372 async fn test_point_origin_coincident_preserves_order() {
10373 let initial_source = "\
10374sketch(on = XY) {
10375 point(at = [var 1, var 2])
10376}
10377";
10378
10379 for (origin_first, snapshot_name) in [
10380 (true, "test_point_origin_coincident_preserves_order_origin_first"),
10381 (false, "test_point_origin_coincident_preserves_order_point_first"),
10382 ] {
10383 let program = Program::parse(initial_source).unwrap().0.unwrap();
10384
10385 let mut frontend = FrontendState::new();
10386
10387 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10388 let mock_ctx = ExecutorContext::new_mock(None).await;
10389 let version = Version(0);
10390
10391 frontend.hack_set_program(&ctx, program).await.unwrap();
10392 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10393 let sketch_id = sketch_object.id;
10394 let sketch = expect_sketch(sketch_object);
10395 let point_id = *sketch.segments.first().unwrap();
10396
10397 let segments = if origin_first {
10398 vec![ConstraintSegment::ORIGIN, point_id.into()]
10399 } else {
10400 vec![point_id.into(), ConstraintSegment::ORIGIN]
10401 };
10402 let constraint = Constraint::Coincident(Coincident {
10403 segments: segments.clone(),
10404 });
10405 let (src_delta, scene_delta) = frontend
10406 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10407 .await
10408 .unwrap();
10409 insta::assert_snapshot!(snapshot_name, src_delta.text.as_str());
10410
10411 let constraint_object = scene_delta
10412 .new_graph
10413 .objects
10414 .iter()
10415 .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
10416 .unwrap();
10417
10418 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10419 panic!("expected a constraint object");
10420 };
10421
10422 assert_eq!(constraint, &Constraint::Coincident(Coincident { segments }));
10423
10424 ctx.close().await;
10425 mock_ctx.close().await;
10426 }
10427 }
10428
10429 #[tokio::test(flavor = "multi_thread")]
10430 async fn test_coincident_of_line_end_points() {
10431 let initial_source = "\
10432sketch(on = XY) {
10433 line(start = [var 1, var 2], end = [var 3, var 4])
10434 line(start = [var 5, var 6], end = [var 7, var 8])
10435}
10436";
10437
10438 let program = Program::parse(initial_source).unwrap().0.unwrap();
10439
10440 let mut frontend = FrontendState::new();
10441
10442 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10443 let mock_ctx = ExecutorContext::new_mock(None).await;
10444 let version = Version(0);
10445
10446 frontend.hack_set_program(&ctx, program).await.unwrap();
10447 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10448 let sketch_id = sketch_object.id;
10449 let sketch = expect_sketch(sketch_object);
10450 let point0_id = *sketch.segments.get(1).unwrap();
10451 let point1_id = *sketch.segments.get(3).unwrap();
10452
10453 let constraint = Constraint::Coincident(Coincident {
10454 segments: vec![point0_id.into(), point1_id.into()],
10455 });
10456 let (src_delta, scene_delta) = frontend
10457 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10458 .await
10459 .unwrap();
10460 insta::assert_snapshot!("test_coincident_of_line_end_points", src_delta.text.as_str());
10461 assert_eq!(
10462 scene_delta.new_graph.objects.len(),
10463 9,
10464 "{:#?}",
10465 scene_delta.new_graph.objects
10466 );
10467
10468 ctx.close().await;
10469 mock_ctx.close().await;
10470 }
10471
10472 #[tokio::test(flavor = "multi_thread")]
10473 async fn test_coincident_of_line_point_and_circle_segment() {
10474 let initial_source = "\
10475sketch(on = XY) {
10476 circle1 = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
10477 line1 = line(start = [var 9mm, var 1mm], end = [var 10mm, var 2mm])
10478}
10479";
10480 let program = Program::parse(initial_source).unwrap().0.unwrap();
10481 let mut frontend = FrontendState::new();
10482
10483 let mock_ctx = ExecutorContext::new_mock(None).await;
10484 let version = Version(0);
10485
10486 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10487 frontend.program = program;
10488 frontend.update_state_after_exec(outcome, true);
10489 let sketch_object = find_first_sketch_object(&frontend.scene_graph).expect("Expected sketch object");
10490 let sketch_id = sketch_object.id;
10491 let sketch = expect_sketch(sketch_object);
10492
10493 let circle_id = sketch
10494 .segments
10495 .iter()
10496 .copied()
10497 .find(|seg_id| {
10498 matches!(
10499 &frontend.scene_graph.objects[seg_id.0].kind,
10500 ObjectKind::Segment {
10501 segment: Segment::Circle(_)
10502 }
10503 )
10504 })
10505 .expect("Expected a circle segment in sketch");
10506 let line_id = frontend
10507 .scene_graph
10508 .objects
10509 .iter()
10510 .find_map(|obj| match &obj.kind {
10511 ObjectKind::Segment {
10512 segment: Segment::Line(line),
10513 } if line.owner.is_none() => Some(obj.id),
10514 _ => None,
10515 })
10516 .expect("Expected a standalone line segment in scene graph");
10517
10518 let line_start_point_id = match &frontend.scene_graph.objects[line_id.0].kind {
10519 ObjectKind::Segment {
10520 segment: Segment::Line(line),
10521 } => line.start,
10522 _ => panic!("Expected line segment object"),
10523 };
10524
10525 let constraint = Constraint::Coincident(Coincident {
10526 segments: vec![line_start_point_id.into(), circle_id.into()],
10527 });
10528 let (src_delta, _scene_delta) = frontend
10529 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10530 .await
10531 .unwrap();
10532 insta::assert_snapshot!(
10533 "test_coincident_of_line_point_and_circle_segment",
10534 src_delta.text.as_str()
10535 );
10536
10537 mock_ctx.close().await;
10538 }
10539
10540 #[tokio::test(flavor = "multi_thread")]
10541 async fn test_invalid_coincident_arc_and_line_preserves_state() {
10542 let program = Program::empty();
10550
10551 let mut frontend = FrontendState::new();
10552 frontend.program = program;
10553
10554 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10555 let mock_ctx = ExecutorContext::new_mock(None).await;
10556 let version = Version(0);
10557
10558 let sketch_args = SketchCtor {
10559 on: Plane::Default(PlaneName::Xy),
10560 };
10561 let (_src_delta, _scene_delta, sketch_id) = frontend
10562 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
10563 .await
10564 .unwrap();
10565
10566 let arc_ctor = ArcCtor {
10568 start: Point2d {
10569 x: Expr::Var(Number {
10570 value: 0.0,
10571 units: NumericSuffix::Mm,
10572 }),
10573 y: Expr::Var(Number {
10574 value: 0.0,
10575 units: NumericSuffix::Mm,
10576 }),
10577 },
10578 end: Point2d {
10579 x: Expr::Var(Number {
10580 value: 10.0,
10581 units: NumericSuffix::Mm,
10582 }),
10583 y: Expr::Var(Number {
10584 value: 10.0,
10585 units: NumericSuffix::Mm,
10586 }),
10587 },
10588 center: Point2d {
10589 x: Expr::Var(Number {
10590 value: 10.0,
10591 units: NumericSuffix::Mm,
10592 }),
10593 y: Expr::Var(Number {
10594 value: 0.0,
10595 units: NumericSuffix::Mm,
10596 }),
10597 },
10598 construction: None,
10599 };
10600 let (_src_delta, scene_delta) = frontend
10601 .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Arc(arc_ctor), None)
10602 .await
10603 .unwrap();
10604 let arc_id = *scene_delta.new_objects.last().unwrap();
10606
10607 let line_ctor = LineCtor {
10609 start: Point2d {
10610 x: Expr::Var(Number {
10611 value: 20.0,
10612 units: NumericSuffix::Mm,
10613 }),
10614 y: Expr::Var(Number {
10615 value: 0.0,
10616 units: NumericSuffix::Mm,
10617 }),
10618 },
10619 end: Point2d {
10620 x: Expr::Var(Number {
10621 value: 30.0,
10622 units: NumericSuffix::Mm,
10623 }),
10624 y: Expr::Var(Number {
10625 value: 10.0,
10626 units: NumericSuffix::Mm,
10627 }),
10628 },
10629 construction: None,
10630 };
10631 let (_src_delta, scene_delta) = frontend
10632 .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Line(line_ctor), None)
10633 .await
10634 .unwrap();
10635 let line_id = *scene_delta.new_objects.last().unwrap();
10637
10638 let constraint = Constraint::Coincident(Coincident {
10641 segments: vec![arc_id.into(), line_id.into()],
10642 });
10643 let result = frontend.add_constraint(&mock_ctx, version, sketch_id, constraint).await;
10644
10645 assert!(result.is_err(), "Expected invalid coincident constraint to fail");
10647
10648 let sketch_object_after =
10651 find_first_sketch_object(&frontend.scene_graph).expect("Sketch should still exist after failed constraint");
10652 let sketch_after = expect_sketch(sketch_object_after);
10653
10654 assert!(
10656 sketch_after.segments.contains(&arc_id),
10657 "Arc segment should still exist after failed constraint"
10658 );
10659 assert!(
10660 sketch_after.segments.contains(&line_id),
10661 "Line segment should still exist after failed constraint"
10662 );
10663
10664 let arc_obj = frontend
10666 .scene_graph
10667 .objects
10668 .get(arc_id.0)
10669 .expect("Arc object should still be accessible");
10670 let line_obj = frontend
10671 .scene_graph
10672 .objects
10673 .get(line_id.0)
10674 .expect("Line object should still be accessible");
10675
10676 match &arc_obj.kind {
10679 ObjectKind::Segment {
10680 segment: Segment::Arc(_),
10681 } => {}
10682 _ => panic!("Arc object should still be an arc segment"),
10683 }
10684 match &line_obj.kind {
10685 ObjectKind::Segment {
10686 segment: Segment::Line(_),
10687 } => {}
10688 _ => panic!("Line object should still be a line segment"),
10689 }
10690
10691 ctx.close().await;
10692 mock_ctx.close().await;
10693 }
10694
10695 #[tokio::test(flavor = "multi_thread")]
10696 async fn test_distance_two_points() {
10697 let initial_source = "\
10698sketch(on = XY) {
10699 point(at = [var 1, var 2])
10700 point(at = [var 3, var 4])
10701}
10702";
10703
10704 let program = Program::parse(initial_source).unwrap().0.unwrap();
10705
10706 let mut frontend = FrontendState::new();
10707
10708 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10709 let mock_ctx = ExecutorContext::new_mock(None).await;
10710 let version = Version(0);
10711
10712 frontend.hack_set_program(&ctx, program).await.unwrap();
10713 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10714 let sketch_id = sketch_object.id;
10715 let sketch = expect_sketch(sketch_object);
10716 let point0_id = *sketch.segments.first().unwrap();
10717 let point1_id = *sketch.segments.get(1).unwrap();
10718
10719 let constraint = Constraint::Distance(Distance {
10720 points: vec![point0_id.into(), point1_id.into()],
10721 distance: Number {
10722 value: 2.0,
10723 units: NumericSuffix::Mm,
10724 },
10725 label_position: None,
10726 source: Default::default(),
10727 });
10728 let (src_delta, scene_delta) = frontend
10729 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10730 .await
10731 .unwrap();
10732 insta::assert_snapshot!("test_distance_two_points", src_delta.text.as_str());
10733 assert_eq!(
10734 scene_delta.new_graph.objects.len(),
10735 5,
10736 "{:#?}",
10737 scene_delta.new_graph.objects
10738 );
10739
10740 ctx.close().await;
10741 mock_ctx.close().await;
10742 }
10743
10744 #[tokio::test(flavor = "multi_thread")]
10745 async fn test_distance_two_points_with_label() {
10746 let initial_source = "\
10747sketch(on = XY) {
10748 point(at = [var 1, var 2])
10749 point(at = [var 3, var 4])
10750}
10751";
10752
10753 let program = Program::parse(initial_source).unwrap().0.unwrap();
10754
10755 let mut frontend = FrontendState::new();
10756
10757 let mock_ctx = ExecutorContext::new_mock(None).await;
10758 let version = Version(0);
10759
10760 frontend.program = program.clone();
10761 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10762 frontend.update_state_after_exec(outcome, true);
10763 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10764 let sketch_id = sketch_object.id;
10765 let sketch = expect_sketch(sketch_object);
10766 let point0_id = *sketch.segments.first().unwrap();
10767 let point1_id = *sketch.segments.get(1).unwrap();
10768
10769 let label_position = Point2d {
10770 x: Number {
10771 value: 10.0,
10772 units: NumericSuffix::Mm,
10773 },
10774 y: Number {
10775 value: 11.0,
10776 units: NumericSuffix::Mm,
10777 },
10778 };
10779 let constraint = Constraint::Distance(Distance {
10780 points: vec![point0_id.into(), point1_id.into()],
10781 distance: Number {
10782 value: 2.0,
10783 units: NumericSuffix::Mm,
10784 },
10785 label_position: Some(label_position.clone()),
10786 source: Default::default(),
10787 });
10788 let (src_delta, scene_delta) = frontend
10789 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10790 .await
10791 .unwrap();
10792 insta::assert_snapshot!("test_distance_two_points_with_label", src_delta.text.as_str());
10793
10794 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10795 let sketch = expect_sketch(sketch_object);
10796 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10797 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10798 panic!("Expected constraint object");
10799 };
10800 let Constraint::Distance(distance) = constraint else {
10801 panic!("Expected distance constraint");
10802 };
10803 assert_eq!(distance.label_position, Some(label_position));
10804
10805 mock_ctx.close().await;
10806 }
10807
10808 #[tokio::test(flavor = "multi_thread")]
10809 async fn test_edit_distance_constraint_label_position() {
10810 let initial_source = "\
10811sketch(on = XY) {
10812 point(at = [var 1, var 2])
10813 point(at = [var 3, var 2])
10814}
10815";
10816
10817 let program = Program::parse(initial_source).unwrap().0.unwrap();
10818
10819 let mut frontend = FrontendState::new();
10820
10821 let mock_ctx = ExecutorContext::new_mock(None).await;
10822 let version = Version(0);
10823
10824 frontend.program = program.clone();
10825 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10826 frontend.update_state_after_exec(outcome, true);
10827 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10828 let sketch_id = sketch_object.id;
10829 let sketch = expect_sketch(sketch_object);
10830 let point0_id = *sketch.segments.first().unwrap();
10831 let point1_id = *sketch.segments.get(1).unwrap();
10832
10833 let constraint = Constraint::Distance(Distance {
10834 points: vec![point0_id.into(), point1_id.into()],
10835 distance: Number {
10836 value: 2.0,
10837 units: NumericSuffix::Mm,
10838 },
10839 label_position: None,
10840 source: Default::default(),
10841 });
10842 let (_, scene_delta) = frontend
10843 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10844 .await
10845 .unwrap();
10846 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10847 let sketch = expect_sketch(sketch_object);
10848 let constraint_id = sketch.constraints[0];
10849 let label_position = Point2d {
10850 x: Number {
10851 value: 10.0,
10852 units: NumericSuffix::Mm,
10853 },
10854 y: Number {
10855 value: 11.0,
10856 units: NumericSuffix::Mm,
10857 },
10858 };
10859
10860 let (src_delta, scene_delta) = frontend
10861 .edit_distance_constraint_label_position(
10862 &mock_ctx,
10863 version,
10864 sketch_id,
10865 constraint_id,
10866 label_position.clone(),
10867 vec![],
10868 )
10869 .await
10870 .unwrap();
10871 insta::assert_snapshot!("test_edit_distance_constraint_label_position", src_delta.text.as_str());
10872
10873 let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
10874 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10875 panic!("Expected constraint object");
10876 };
10877 let Constraint::Distance(distance) = constraint else {
10878 panic!("Expected distance constraint");
10879 };
10880 assert_eq!(distance.label_position, Some(label_position));
10881
10882 mock_ctx.close().await;
10883 }
10884
10885 #[tokio::test(flavor = "multi_thread")]
10886 async fn test_edit_segments_can_commit_constraint_label_position_in_same_execution() {
10887 let initial_source = "\
10888@settings(kclVersion = 2.0)
10889
10890sketch001 = sketch(on = XZ) {
10891 line1 = line(start = [var 0mm, var 12.55mm], end = [var -6.03mm, var 8.51mm])
10892 line3 = line(start = [var -7.41mm, var 2.92mm], end = [var -1.47mm, var 4.32mm])
10893 distance([line1.start, line3.end], labelPosition = [5.56mm, 8.65mm]) == 8.36mm
10894 vertical([line1.start, ORIGIN])
10895}
10896";
10897
10898 let program = Program::parse(initial_source).unwrap().0.unwrap();
10899 let mut frontend = FrontendState::new();
10900 let mock_ctx = ExecutorContext::new_mock(None).await;
10901 let version = Version(0);
10902
10903 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10904 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10905 let sketch_id = sketch_object.id;
10906 let sketch = expect_sketch(sketch_object);
10907 let constraint_id = sketch
10908 .constraints
10909 .iter()
10910 .copied()
10911 .find(|constraint_id| {
10912 matches!(
10913 frontend.scene_graph.objects[constraint_id.0].kind,
10914 ObjectKind::Constraint {
10915 constraint: Constraint::Distance(_)
10916 }
10917 )
10918 })
10919 .unwrap();
10920 let line1_id = sketch
10921 .segments
10922 .iter()
10923 .copied()
10924 .find(|segment_id| {
10925 matches!(
10926 frontend.scene_graph.objects[segment_id.0].kind,
10927 ObjectKind::Segment {
10928 segment: Segment::Line(_)
10929 }
10930 )
10931 })
10932 .unwrap();
10933 let label_position = Point2d {
10934 x: Number {
10935 value: 7.0,
10936 units: NumericSuffix::Mm,
10937 },
10938 y: Number {
10939 value: 9.0,
10940 units: NumericSuffix::Mm,
10941 },
10942 };
10943
10944 let (source_delta, scene_delta) = frontend
10945 .edit_segments_with_options(
10946 &mock_ctx,
10947 version,
10948 sketch_id,
10949 vec![ExistingSegmentCtor {
10950 id: line1_id,
10951 ctor: SegmentCtor::Line(LineCtor {
10952 start: point_expr_mm(2.0, 15.55),
10953 end: point_expr_mm(-4.03, 11.51),
10954 construction: None,
10955 }),
10956 }],
10957 EditSegmentsOptions {
10958 anchor_segment_ids: Some(vec![]),
10959 drag_anchors: vec![SegmentDragAnchor {
10960 segment_id: line1_id,
10961 target: label_position.clone(),
10962 }],
10963 constraint_label_edits: vec![ConstraintLabelPositionEdit {
10964 constraint_id,
10965 label_position: label_position.clone(),
10966 }],
10967 commit_solved_initial_guesses: true,
10968 },
10969 )
10970 .await
10971 .unwrap();
10972
10973 assert!(source_delta.text.contains("labelPosition = [7mm, 9mm]"));
10974 let constraint_object = &scene_delta.new_graph.objects[constraint_id.0];
10975 let ObjectKind::Constraint {
10976 constraint: Constraint::Distance(distance),
10977 } = &constraint_object.kind
10978 else {
10979 panic!("Expected distance constraint object");
10980 };
10981 assert_eq!(distance.label_position, Some(label_position));
10982
10983 let snapped_label_position = Point2d {
10984 x: Number {
10985 value: 8.0,
10986 units: NumericSuffix::Mm,
10987 },
10988 y: Number {
10989 value: 10.0,
10990 units: NumericSuffix::Mm,
10991 },
10992 };
10993 let (source_delta, scene_delta) = frontend
10994 .edit_segments_with_options(
10995 &mock_ctx,
10996 version,
10997 sketch_id,
10998 vec![],
10999 EditSegmentsOptions {
11000 anchor_segment_ids: Some(vec![line1_id]),
11001 drag_anchors: vec![],
11002 constraint_label_edits: vec![ConstraintLabelPositionEdit {
11003 constraint_id,
11004 label_position: snapped_label_position.clone(),
11005 }],
11006 commit_solved_initial_guesses: true,
11007 },
11008 )
11009 .await
11010 .unwrap();
11011
11012 assert!(source_delta.text.contains("labelPosition = [8mm, 10mm]"));
11013 let constraint_object = &scene_delta.new_graph.objects[constraint_id.0];
11014 let ObjectKind::Constraint {
11015 constraint: Constraint::Distance(distance),
11016 } = &constraint_object.kind
11017 else {
11018 panic!("Expected distance constraint object");
11019 };
11020 assert_eq!(distance.label_position, Some(snapped_label_position));
11021
11022 mock_ctx.close().await;
11023 }
11024
11025 #[tokio::test(flavor = "multi_thread")]
11026 async fn test_edit_distance_constraint_label_position_preserves_anchor_segment_solution() {
11027 let initial_source = "\
11028sketch(on = XY) {
11029 point1 = point(at = [var 0mm, var 0mm])
11030 point2 = point(at = [var 10mm, var 0mm])
11031 distance([point1, point2]) == 5mm
11032}
11033";
11034
11035 let program = Program::parse(initial_source).unwrap().0.unwrap();
11036 let mut frontend = FrontendState::new();
11037 let mock_ctx = ExecutorContext::new_mock(None).await;
11038 let version = Version(0);
11039
11040 frontend.program = program.clone();
11041 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11042 frontend.update_state_after_exec(outcome, true);
11043 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11044 let sketch_id = sketch_object.id;
11045 let sketch = expect_sketch(sketch_object);
11046 let point0_id = sketch.segments[0];
11047 let point1_id = sketch.segments[1];
11048 let constraint_id = sketch.constraints[0];
11049
11050 let edited_segments = vec![ExistingSegmentCtor {
11051 id: point0_id,
11052 ctor: SegmentCtor::Point(PointCtor {
11053 position: Point2d {
11054 x: Expr::Var(Number {
11055 value: 2.0,
11056 units: NumericSuffix::Mm,
11057 }),
11058 y: Expr::Var(Number {
11059 value: 1.0,
11060 units: NumericSuffix::Mm,
11061 }),
11062 },
11063 }),
11064 }];
11065 let (_, scene_delta) = frontend
11066 .edit_segments(&mock_ctx, version, sketch_id, edited_segments)
11067 .await
11068 .unwrap();
11069 let point0_after_segment_edit = point_position(&scene_delta.new_graph, point0_id);
11070 let point1_after_segment_edit = point_position(&scene_delta.new_graph, point1_id);
11071
11072 let label_position = Point2d {
11073 x: Number {
11074 value: 3.0,
11075 units: NumericSuffix::Mm,
11076 },
11077 y: Number {
11078 value: 4.0,
11079 units: NumericSuffix::Mm,
11080 },
11081 };
11082 let (_, scene_delta) = frontend
11083 .edit_distance_constraint_label_position(
11084 &mock_ctx,
11085 version,
11086 sketch_id,
11087 constraint_id,
11088 label_position,
11089 vec![point0_id],
11090 )
11091 .await
11092 .unwrap();
11093
11094 assert_point_position_close(
11095 point_position(&scene_delta.new_graph, point0_id),
11096 point0_after_segment_edit,
11097 );
11098 assert_point_position_close(
11099 point_position(&scene_delta.new_graph, point1_id),
11100 point1_after_segment_edit,
11101 );
11102
11103 mock_ctx.close().await;
11104 }
11105
11106 #[tokio::test(flavor = "multi_thread")]
11107 async fn test_distance_point_line() {
11108 let initial_source = "\
11109sketch(on = XY) {
11110 point(at = [var 0, var 5])
11111 line(start = [var 0, var 0], end = [var 10, var 0])
11112}
11113";
11114
11115 let program = Program::parse(initial_source).unwrap().0.unwrap();
11116
11117 let mut frontend = FrontendState::new();
11118
11119 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11120 let mock_ctx = ExecutorContext::new_mock(None).await;
11121 let version = Version(0);
11122
11123 frontend.hack_set_program(&ctx, program).await.unwrap();
11124 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11125 let sketch_id = sketch_object.id;
11126 let sketch = expect_sketch(sketch_object);
11127 let point_id = *sketch.segments.first().unwrap();
11128 let line_id = *sketch
11129 .segments
11130 .iter()
11131 .find(|segment_id| {
11132 matches!(
11133 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11134 Some(ObjectKind::Segment {
11135 segment: Segment::Line(_)
11136 })
11137 )
11138 })
11139 .unwrap();
11140
11141 let label_position = Point2d {
11142 x: Number {
11143 value: 10.0,
11144 units: NumericSuffix::Mm,
11145 },
11146 y: Number {
11147 value: 11.0,
11148 units: NumericSuffix::Mm,
11149 },
11150 };
11151 let constraint = Constraint::Distance(Distance {
11152 points: vec![point_id.into(), line_id.into()],
11153 distance: Number {
11154 value: 5.0,
11155 units: NumericSuffix::Mm,
11156 },
11157 label_position: Some(label_position.clone()),
11158 source: Default::default(),
11159 });
11160 let (src_delta, scene_delta) = frontend
11161 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11162 .await
11163 .unwrap();
11164 insta::assert_snapshot!("test_distance_point_line", src_delta.text.as_str());
11165 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
11166 let sketch = expect_sketch(sketch_object);
11167 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
11168 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11169 panic!("Expected constraint object");
11170 };
11171 let Constraint::Distance(distance) = constraint else {
11172 panic!("Expected distance constraint");
11173 };
11174 assert_eq!(distance.label_position, Some(label_position));
11175
11176 ctx.close().await;
11177 mock_ctx.close().await;
11178 }
11179
11180 #[tokio::test(flavor = "multi_thread")]
11181 async fn test_distance_point_arc() {
11182 let initial_source = "\
11183sketch(on = XY) {
11184 point(at = [var 0, var 8])
11185 arc(start = [var 5, var 0], end = [var 0, var 5], center = [var 0, var 0])
11186}
11187";
11188
11189 let program = Program::parse(initial_source).unwrap().0.unwrap();
11190
11191 let mut frontend = FrontendState::new();
11192
11193 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11194 let mock_ctx = ExecutorContext::new_mock(None).await;
11195 let version = Version(0);
11196
11197 frontend.hack_set_program(&ctx, program).await.unwrap();
11198 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11199 let sketch_id = sketch_object.id;
11200 let sketch = expect_sketch(sketch_object);
11201 let point_id = *sketch.segments.first().unwrap();
11202 let arc_id = *sketch
11203 .segments
11204 .iter()
11205 .find(|segment_id| {
11206 matches!(
11207 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11208 Some(ObjectKind::Segment {
11209 segment: Segment::Arc(_)
11210 })
11211 )
11212 })
11213 .unwrap();
11214
11215 let constraint = Constraint::Distance(Distance {
11216 points: vec![point_id.into(), arc_id.into()],
11217 distance: Number {
11218 value: 3.0,
11219 units: NumericSuffix::Mm,
11220 },
11221 label_position: None,
11222 source: Default::default(),
11223 });
11224 let (src_delta, _scene_delta) = frontend
11225 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11226 .await
11227 .unwrap();
11228 insta::assert_snapshot!("test_distance_point_arc", src_delta.text.as_str());
11229
11230 ctx.close().await;
11231 mock_ctx.close().await;
11232 }
11233
11234 #[tokio::test(flavor = "multi_thread")]
11235 async fn test_distance_arc_origin() {
11236 let initial_source = "\
11237sketch001 = sketch(on = XY) {
11238 arc(start = [var -4.13mm, var -0.59mm], end = [var -3.47mm, var 3.38mm], center = [var -4.55mm, var 1.52mm])
11239}
11240";
11241
11242 let program = Program::parse(initial_source).unwrap().0.unwrap();
11243
11244 let mut frontend = FrontendState::new();
11245
11246 let mock_ctx = ExecutorContext::new_mock(None).await;
11247 let version = Version(0);
11248
11249 frontend.program = program.clone();
11250 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11251 frontend.update_state_after_exec(outcome, true);
11252 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11253 let sketch_id = sketch_object.id;
11254 let sketch = expect_sketch(sketch_object);
11255 let arc_id = *sketch
11256 .segments
11257 .iter()
11258 .find(|segment_id| {
11259 matches!(
11260 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11261 Some(ObjectKind::Segment {
11262 segment: Segment::Arc(_)
11263 })
11264 )
11265 })
11266 .unwrap();
11267
11268 let constraint = Constraint::Distance(Distance {
11269 points: vec![arc_id.into(), ConstraintSegment::ORIGIN],
11270 distance: Number {
11271 value: 3.0,
11272 units: NumericSuffix::Mm,
11273 },
11274 label_position: None,
11275 source: Default::default(),
11276 });
11277 let (src_delta, _scene_delta) = frontend
11278 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11279 .await
11280 .unwrap();
11281 insta::assert_snapshot!("test_distance_arc_origin", src_delta.text.as_str());
11282
11283 mock_ctx.close().await;
11284 }
11285
11286 #[tokio::test(flavor = "multi_thread")]
11287 async fn test_distance_line_origin() {
11288 let initial_source = "\
11289sketch(on = XY) {
11290 line(start = [var 5, var 0], end = [var 5, var 10])
11291}
11292";
11293
11294 let program = Program::parse(initial_source).unwrap().0.unwrap();
11295
11296 let mut frontend = FrontendState::new();
11297
11298 let mock_ctx = ExecutorContext::new_mock(None).await;
11299 let version = Version(0);
11300
11301 frontend.program = program.clone();
11302 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11303 frontend.update_state_after_exec(outcome, true);
11304 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11305 let sketch_id = sketch_object.id;
11306 let sketch = expect_sketch(sketch_object);
11307 let line_id = *sketch
11308 .segments
11309 .iter()
11310 .find(|segment_id| {
11311 matches!(
11312 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11313 Some(ObjectKind::Segment {
11314 segment: Segment::Line(_)
11315 })
11316 )
11317 })
11318 .unwrap();
11319
11320 let constraint = Constraint::Distance(Distance {
11321 points: vec![ConstraintSegment::ORIGIN, line_id.into()],
11322 distance: Number {
11323 value: 5.0,
11324 units: NumericSuffix::Mm,
11325 },
11326 label_position: None,
11327 source: Default::default(),
11328 });
11329 let (src_delta, _scene_delta) = frontend
11330 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11331 .await
11332 .unwrap();
11333 insta::assert_snapshot!("test_distance_line_origin", src_delta.text.as_str());
11334
11335 mock_ctx.close().await;
11336 }
11337
11338 #[tokio::test(flavor = "multi_thread")]
11339 async fn test_distance_line_circle() {
11340 let initial_source = "\
11341sketch(on = XY) {
11342 line(start = [var -10, var 8], end = [var 10, var 8])
11343 circle(start = [var 5, var 0], center = [var 0, var 0])
11344}
11345";
11346
11347 let program = Program::parse(initial_source).unwrap().0.unwrap();
11348
11349 let mut frontend = FrontendState::new();
11350
11351 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11352 let mock_ctx = ExecutorContext::new_mock(None).await;
11353 let version = Version(0);
11354
11355 frontend.hack_set_program(&ctx, program).await.unwrap();
11356 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11357 let sketch_id = sketch_object.id;
11358 let sketch = expect_sketch(sketch_object);
11359 let line_id = *sketch
11360 .segments
11361 .iter()
11362 .find(|segment_id| {
11363 matches!(
11364 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11365 Some(ObjectKind::Segment {
11366 segment: Segment::Line(_)
11367 })
11368 )
11369 })
11370 .unwrap();
11371 let circle_id = *sketch
11372 .segments
11373 .iter()
11374 .find(|segment_id| {
11375 matches!(
11376 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11377 Some(ObjectKind::Segment {
11378 segment: Segment::Circle(_)
11379 })
11380 )
11381 })
11382 .unwrap();
11383
11384 let constraint = Constraint::Distance(Distance {
11385 points: vec![line_id.into(), circle_id.into()],
11386 distance: Number {
11387 value: 3.0,
11388 units: NumericSuffix::Mm,
11389 },
11390 label_position: None,
11391 source: Default::default(),
11392 });
11393 let (src_delta, _scene_delta) = frontend
11394 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11395 .await
11396 .unwrap();
11397 insta::assert_snapshot!("test_distance_line_circle", src_delta.text.as_str());
11398
11399 ctx.close().await;
11400 mock_ctx.close().await;
11401 }
11402
11403 #[tokio::test(flavor = "multi_thread")]
11404 async fn test_distance_circle_arc() {
11405 let initial_source = "\
11406sketch(on = XY) {
11407 circle(start = [var 5, var 0], center = [var 0, var 0])
11408 arc(start = [var 15, var 0], end = [var 10, var 5], center = [var 10, var 0])
11409}
11410";
11411
11412 let program = Program::parse(initial_source).unwrap().0.unwrap();
11413
11414 let mut frontend = FrontendState::new();
11415
11416 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11417 let mock_ctx = ExecutorContext::new_mock(None).await;
11418 let version = Version(0);
11419
11420 frontend.hack_set_program(&ctx, program).await.unwrap();
11421 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11422 let sketch_id = sketch_object.id;
11423 let sketch = expect_sketch(sketch_object);
11424 let circle_id = *sketch
11425 .segments
11426 .iter()
11427 .find(|segment_id| {
11428 matches!(
11429 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11430 Some(ObjectKind::Segment {
11431 segment: Segment::Circle(_)
11432 })
11433 )
11434 })
11435 .unwrap();
11436 let arc_id = *sketch
11437 .segments
11438 .iter()
11439 .find(|segment_id| {
11440 matches!(
11441 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11442 Some(ObjectKind::Segment {
11443 segment: Segment::Arc(_)
11444 })
11445 )
11446 })
11447 .unwrap();
11448
11449 let constraint = Constraint::Distance(Distance {
11450 points: vec![circle_id.into(), arc_id.into()],
11451 distance: Number {
11452 value: 3.0,
11453 units: NumericSuffix::Mm,
11454 },
11455 label_position: None,
11456 source: Default::default(),
11457 });
11458 let (src_delta, _scene_delta) = frontend
11459 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11460 .await
11461 .unwrap();
11462 insta::assert_snapshot!("test_distance_circle_arc", src_delta.text.as_str());
11463
11464 ctx.close().await;
11465 mock_ctx.close().await;
11466 }
11467
11468 #[tokio::test(flavor = "multi_thread")]
11469 async fn test_distance_parallel_lines() {
11470 let initial_source = "\
11471sketch(on = XY) {
11472 line(start = [var 0, var 0], end = [var 10, var 0])
11473 line(start = [var 0, var 5], end = [var 10, var 5])
11474}
11475";
11476
11477 let program = Program::parse(initial_source).unwrap().0.unwrap();
11478
11479 let mut frontend = FrontendState::new();
11480
11481 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11482 let mock_ctx = ExecutorContext::new_mock(None).await;
11483 let version = Version(0);
11484
11485 frontend.hack_set_program(&ctx, program).await.unwrap();
11486 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11487 let sketch_id = sketch_object.id;
11488 let sketch = expect_sketch(sketch_object);
11489 let line_ids = sketch
11490 .segments
11491 .iter()
11492 .copied()
11493 .filter(|segment_id| {
11494 matches!(
11495 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11496 Some(ObjectKind::Segment {
11497 segment: Segment::Line(_)
11498 })
11499 )
11500 })
11501 .collect::<Vec<_>>();
11502
11503 let constraint = Constraint::Distance(Distance {
11504 points: vec![line_ids[0].into(), line_ids[1].into()],
11505 distance: Number {
11506 value: 5.0,
11507 units: NumericSuffix::Mm,
11508 },
11509 label_position: None,
11510 source: Default::default(),
11511 });
11512 let (src_delta, _scene_delta) = frontend
11513 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11514 .await
11515 .unwrap();
11516 insta::assert_snapshot!("test_distance_parallel_lines", src_delta.text.as_str());
11517
11518 ctx.close().await;
11519 mock_ctx.close().await;
11520 }
11521
11522 #[tokio::test(flavor = "multi_thread")]
11523 async fn test_distance_non_parallel_lines_lowers_to_distance() {
11524 let initial_source = "\
11525sketch(on = XY) {
11526 line(start = [var 0, var 0], end = [var 10, var 0])
11527 line(start = [var 0, var 0], end = [var 0, var 10])
11528}
11529";
11530
11531 let program = Program::parse(initial_source).unwrap().0.unwrap();
11532
11533 let mut frontend = FrontendState::new();
11534
11535 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11536 let mock_ctx = ExecutorContext::new_mock(None).await;
11537 let version = Version(0);
11538
11539 frontend.hack_set_program(&ctx, program).await.unwrap();
11540 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11541 let sketch_id = sketch_object.id;
11542 let sketch = expect_sketch(sketch_object);
11543 let line_ids = sketch
11544 .segments
11545 .iter()
11546 .copied()
11547 .filter(|segment_id| {
11548 matches!(
11549 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11550 Some(ObjectKind::Segment {
11551 segment: Segment::Line(_)
11552 })
11553 )
11554 })
11555 .collect::<Vec<_>>();
11556
11557 let constraint = Constraint::Distance(Distance {
11558 points: vec![line_ids[0].into(), line_ids[1].into()],
11559 distance: Number {
11560 value: 5.0,
11561 units: NumericSuffix::Mm,
11562 },
11563 label_position: None,
11564 source: Default::default(),
11565 });
11566 let (src_delta, _scene_delta) = frontend
11567 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11568 .await
11569 .unwrap();
11570 insta::assert_snapshot!(
11571 "test_distance_non_parallel_lines_lowers_to_distance",
11572 src_delta.text.as_str()
11573 );
11574
11575 ctx.close().await;
11576 mock_ctx.close().await;
11577 }
11578
11579 #[tokio::test(flavor = "multi_thread")]
11580 async fn test_horizontal_distance_two_points() {
11581 let initial_source = "\
11582sketch(on = XY) {
11583 point(at = [var 1, var 2])
11584 point(at = [var 3, var 4])
11585}
11586";
11587
11588 let program = Program::parse(initial_source).unwrap().0.unwrap();
11589
11590 let mut frontend = FrontendState::new();
11591
11592 let mock_ctx = ExecutorContext::new_mock(None).await;
11593 let version = Version(0);
11594
11595 frontend.program = program.clone();
11596 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11597 frontend.update_state_after_exec(outcome, true);
11598 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11599 let sketch_id = sketch_object.id;
11600 let sketch = expect_sketch(sketch_object);
11601 let point0_id = *sketch.segments.first().unwrap();
11602 let point1_id = *sketch.segments.get(1).unwrap();
11603 let label_position = Point2d {
11604 x: Number {
11605 value: 10.0,
11606 units: NumericSuffix::Mm,
11607 },
11608 y: Number {
11609 value: 11.0,
11610 units: NumericSuffix::Mm,
11611 },
11612 };
11613
11614 let constraint = Constraint::HorizontalDistance(Distance {
11615 points: vec![point0_id.into(), point1_id.into()],
11616 distance: Number {
11617 value: 2.0,
11618 units: NumericSuffix::Mm,
11619 },
11620 label_position: Some(label_position.clone()),
11621 source: Default::default(),
11622 });
11623 let (src_delta, scene_delta) = frontend
11624 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11625 .await
11626 .unwrap();
11627 insta::assert_snapshot!("test_horizontal_distance_two_points", src_delta.text.as_str());
11628 assert_eq!(
11629 scene_delta.new_graph.objects.len(),
11630 5,
11631 "{:#?}",
11632 scene_delta.new_graph.objects
11633 );
11634 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
11635 let sketch = expect_sketch(sketch_object);
11636 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
11637 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11638 panic!("Expected constraint object");
11639 };
11640 let Constraint::HorizontalDistance(distance) = constraint else {
11641 panic!("Expected horizontal distance constraint");
11642 };
11643 assert_eq!(distance.label_position, Some(label_position));
11644
11645 mock_ctx.close().await;
11646 }
11647
11648 #[tokio::test(flavor = "multi_thread")]
11649 async fn test_radius_single_arc_segment() {
11650 let initial_source = "\
11651sketch(on = XY) {
11652 arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
11653}
11654";
11655
11656 let program = Program::parse(initial_source).unwrap().0.unwrap();
11657
11658 let mut frontend = FrontendState::new();
11659
11660 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11661 let mock_ctx = ExecutorContext::new_mock(None).await;
11662 let version = Version(0);
11663
11664 frontend.hack_set_program(&ctx, program).await.unwrap();
11665 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11666 let sketch_id = sketch_object.id;
11667 let sketch = expect_sketch(sketch_object);
11668 let arc_id = sketch
11670 .segments
11671 .iter()
11672 .find(|&seg_id| {
11673 let obj = frontend.scene_graph.objects.get(seg_id.0);
11674 matches!(
11675 obj.map(|o| &o.kind),
11676 Some(ObjectKind::Segment {
11677 segment: Segment::Arc(_)
11678 })
11679 )
11680 })
11681 .unwrap();
11682
11683 let constraint = Constraint::Radius(Radius {
11684 arc: *arc_id,
11685 radius: Number {
11686 value: 5.0,
11687 units: NumericSuffix::Mm,
11688 },
11689 label_position: None,
11690 source: Default::default(),
11691 });
11692 let (src_delta, scene_delta) = frontend
11693 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11694 .await
11695 .unwrap();
11696 insta::assert_snapshot!("test_radius_single_arc_segment", src_delta.text.as_str());
11697 assert_eq!(
11698 scene_delta.new_graph.objects.len(),
11699 7, "{:#?}",
11701 scene_delta.new_graph.objects
11702 );
11703
11704 ctx.close().await;
11705 mock_ctx.close().await;
11706 }
11707
11708 #[tokio::test(flavor = "multi_thread")]
11709 async fn test_radius_single_arc_segment_with_label_position() {
11710 let initial_source = "\
11711sketch(on = XY) {
11712 arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
11713}
11714";
11715
11716 let program = Program::parse(initial_source).unwrap().0.unwrap();
11717 let mut frontend = FrontendState::new();
11718 let mock_ctx = ExecutorContext::new_mock(None).await;
11719 let version = Version(0);
11720
11721 frontend.program = program.clone();
11722 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11723 frontend.update_state_after_exec(outcome, true);
11724 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11725 let sketch_id = sketch_object.id;
11726 let sketch = expect_sketch(sketch_object);
11727 let arc_id = sketch
11728 .segments
11729 .iter()
11730 .find(|&seg_id| {
11731 let obj = frontend.scene_graph.objects.get(seg_id.0);
11732 matches!(
11733 obj.map(|o| &o.kind),
11734 Some(ObjectKind::Segment {
11735 segment: Segment::Arc(_)
11736 })
11737 )
11738 })
11739 .unwrap();
11740
11741 let label_position = Point2d {
11742 x: Number {
11743 value: 10.0,
11744 units: NumericSuffix::Mm,
11745 },
11746 y: Number {
11747 value: 11.0,
11748 units: NumericSuffix::Mm,
11749 },
11750 };
11751 let constraint = Constraint::Radius(Radius {
11752 arc: *arc_id,
11753 radius: Number {
11754 value: 5.0,
11755 units: NumericSuffix::Mm,
11756 },
11757 label_position: Some(label_position.clone()),
11758 source: Default::default(),
11759 });
11760 let (src_delta, scene_delta) = frontend
11761 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11762 .await
11763 .unwrap();
11764 insta::assert_snapshot!(
11765 "test_radius_single_arc_segment_with_label_position",
11766 src_delta.text.as_str()
11767 );
11768
11769 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
11770 let sketch = expect_sketch(sketch_object);
11771 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
11772 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11773 panic!("Expected constraint object");
11774 };
11775 let Constraint::Radius(radius) = constraint else {
11776 panic!("Expected radius constraint");
11777 };
11778 assert_eq!(radius.label_position, Some(label_position));
11779
11780 mock_ctx.close().await;
11781 }
11782
11783 #[tokio::test(flavor = "multi_thread")]
11784 async fn test_edit_radius_constraint_label_position() {
11785 let initial_source = "\
11786sketch(on = XY) {
11787 arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
11788 radius(arc1) == 5mm
11789}
11790";
11791
11792 let program = Program::parse(initial_source).unwrap().0.unwrap();
11793 let mut frontend = FrontendState::new();
11794 let mock_ctx = ExecutorContext::new_mock(None).await;
11795 let version = Version(0);
11796
11797 frontend.program = program.clone();
11798 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11799 frontend.update_state_after_exec(outcome, true);
11800 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11801 let sketch_id = sketch_object.id;
11802 let sketch = expect_sketch(sketch_object);
11803 let constraint_id = sketch.constraints[0];
11804 let label_position = Point2d {
11805 x: Number {
11806 value: 10.0,
11807 units: NumericSuffix::Mm,
11808 },
11809 y: Number {
11810 value: 11.0,
11811 units: NumericSuffix::Mm,
11812 },
11813 };
11814
11815 let (src_delta, scene_delta) = frontend
11816 .edit_distance_constraint_label_position(
11817 &mock_ctx,
11818 version,
11819 sketch_id,
11820 constraint_id,
11821 label_position.clone(),
11822 vec![],
11823 )
11824 .await
11825 .unwrap();
11826 insta::assert_snapshot!("test_edit_radius_constraint_label_position", src_delta.text.as_str());
11827
11828 let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
11829 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11830 panic!("Expected constraint object");
11831 };
11832 let Constraint::Radius(radius) = constraint else {
11833 panic!("Expected radius constraint");
11834 };
11835 assert_eq!(radius.label_position, Some(label_position));
11836
11837 mock_ctx.close().await;
11838 }
11839
11840 #[tokio::test(flavor = "multi_thread")]
11841 async fn test_vertical_distance_two_points() {
11842 let initial_source = "\
11843sketch(on = XY) {
11844 point(at = [var 1, var 2])
11845 point(at = [var 3, var 4])
11846}
11847";
11848
11849 let program = Program::parse(initial_source).unwrap().0.unwrap();
11850
11851 let mut frontend = FrontendState::new();
11852
11853 let mock_ctx = ExecutorContext::new_mock(None).await;
11854 let version = Version(0);
11855
11856 frontend.program = program.clone();
11857 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11858 frontend.update_state_after_exec(outcome, true);
11859 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11860 let sketch_id = sketch_object.id;
11861 let sketch = expect_sketch(sketch_object);
11862 let point0_id = *sketch.segments.first().unwrap();
11863 let point1_id = *sketch.segments.get(1).unwrap();
11864 let label_position = Point2d {
11865 x: Number {
11866 value: 10.0,
11867 units: NumericSuffix::Mm,
11868 },
11869 y: Number {
11870 value: 11.0,
11871 units: NumericSuffix::Mm,
11872 },
11873 };
11874
11875 let constraint = Constraint::VerticalDistance(Distance {
11876 points: vec![point0_id.into(), point1_id.into()],
11877 distance: Number {
11878 value: 2.0,
11879 units: NumericSuffix::Mm,
11880 },
11881 label_position: Some(label_position.clone()),
11882 source: Default::default(),
11883 });
11884 let (src_delta, scene_delta) = frontend
11885 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11886 .await
11887 .unwrap();
11888 insta::assert_snapshot!("test_vertical_distance_two_points", src_delta.text.as_str());
11889 assert_eq!(
11890 scene_delta.new_graph.objects.len(),
11891 5,
11892 "{:#?}",
11893 scene_delta.new_graph.objects
11894 );
11895 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
11896 let sketch = expect_sketch(sketch_object);
11897 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
11898 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11899 panic!("Expected constraint object");
11900 };
11901 let Constraint::VerticalDistance(distance) = constraint else {
11902 panic!("Expected vertical distance constraint");
11903 };
11904 assert_eq!(distance.label_position, Some(label_position));
11905
11906 mock_ctx.close().await;
11907 }
11908
11909 #[tokio::test(flavor = "multi_thread")]
11910 async fn test_add_fixed_standalone_point() {
11911 let initial_source = "\
11912sketch(on = XY) {
11913 point(at = [var 1, var 2])
11914}
11915";
11916
11917 let program = Program::parse(initial_source).unwrap().0.unwrap();
11918
11919 let mut frontend = FrontendState::new();
11920
11921 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11922 let mock_ctx = ExecutorContext::new_mock(None).await;
11923 let version = Version(0);
11924
11925 frontend.hack_set_program(&ctx, program).await.unwrap();
11926 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11927 let sketch_id = sketch_object.id;
11928 let sketch = expect_sketch(sketch_object);
11929 let point_id = *sketch.segments.first().unwrap();
11930
11931 let (src_delta, scene_delta) = frontend
11932 .add_constraint(
11933 &mock_ctx,
11934 version,
11935 sketch_id,
11936 Constraint::Fixed(Fixed {
11937 points: vec![FixedPoint {
11938 point: point_id,
11939 position: Point2d {
11940 x: Number {
11941 value: 2.0,
11942 units: NumericSuffix::Mm,
11943 },
11944 y: Number {
11945 value: 3.0,
11946 units: NumericSuffix::Mm,
11947 },
11948 },
11949 }],
11950 }),
11951 )
11952 .await
11953 .unwrap();
11954 insta::assert_snapshot!("test_add_fixed_standalone_point", src_delta.text.as_str());
11955 assert_eq!(
11956 scene_delta.new_graph.objects.len(),
11957 4,
11958 "{:#?}",
11959 scene_delta.new_graph.objects
11960 );
11961
11962 ctx.close().await;
11963 mock_ctx.close().await;
11964 }
11965
11966 #[tokio::test(flavor = "multi_thread")]
11967 async fn test_add_fixed_multiple_points() {
11968 let initial_source = "\
11969sketch(on = XY) {
11970 point(at = [var 1, var 2])
11971 point(at = [var 3, var 4])
11972}
11973";
11974
11975 let program = Program::parse(initial_source).unwrap().0.unwrap();
11976
11977 let mut frontend = FrontendState::new();
11978
11979 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11980 let mock_ctx = ExecutorContext::new_mock(None).await;
11981 let version = Version(0);
11982
11983 frontend.hack_set_program(&ctx, program).await.unwrap();
11984 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11985 let sketch_id = sketch_object.id;
11986 let sketch = expect_sketch(sketch_object);
11987 let point0_id = *sketch.segments.first().unwrap();
11988 let point1_id = *sketch.segments.get(1).unwrap();
11989
11990 let (src_delta, scene_delta) = frontend
11991 .add_constraint(
11992 &mock_ctx,
11993 version,
11994 sketch_id,
11995 Constraint::Fixed(Fixed {
11996 points: vec![
11997 FixedPoint {
11998 point: point0_id,
11999 position: Point2d {
12000 x: Number {
12001 value: 2.0,
12002 units: NumericSuffix::Mm,
12003 },
12004 y: Number {
12005 value: 3.0,
12006 units: NumericSuffix::Mm,
12007 },
12008 },
12009 },
12010 FixedPoint {
12011 point: point1_id,
12012 position: Point2d {
12013 x: Number {
12014 value: 4.0,
12015 units: NumericSuffix::Mm,
12016 },
12017 y: Number {
12018 value: 5.0,
12019 units: NumericSuffix::Mm,
12020 },
12021 },
12022 },
12023 ],
12024 }),
12025 )
12026 .await
12027 .unwrap();
12028 insta::assert_snapshot!("test_add_fixed_multiple_points", src_delta.text.as_str());
12029 assert_eq!(
12030 scene_delta.new_graph.objects.len(),
12031 6,
12032 "{:#?}",
12033 scene_delta.new_graph.objects
12034 );
12035
12036 ctx.close().await;
12037 mock_ctx.close().await;
12038 }
12039
12040 #[tokio::test(flavor = "multi_thread")]
12041 async fn test_add_fixed_owned_point() {
12042 let initial_source = "\
12043sketch(on = XY) {
12044 line(start = [var 1, var 2], end = [var 3, var 4])
12045}
12046";
12047
12048 let program = Program::parse(initial_source).unwrap().0.unwrap();
12049
12050 let mut frontend = FrontendState::new();
12051
12052 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12053 let mock_ctx = ExecutorContext::new_mock(None).await;
12054 let version = Version(0);
12055
12056 frontend.hack_set_program(&ctx, program).await.unwrap();
12057 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12058 let sketch_id = sketch_object.id;
12059 let sketch = expect_sketch(sketch_object);
12060 let line_start_id = *sketch.segments.first().unwrap();
12061
12062 let (src_delta, scene_delta) = frontend
12063 .add_constraint(
12064 &mock_ctx,
12065 version,
12066 sketch_id,
12067 Constraint::Fixed(Fixed {
12068 points: vec![FixedPoint {
12069 point: line_start_id,
12070 position: Point2d {
12071 x: Number {
12072 value: 2.0,
12073 units: NumericSuffix::Mm,
12074 },
12075 y: Number {
12076 value: 3.0,
12077 units: NumericSuffix::Mm,
12078 },
12079 },
12080 }],
12081 }),
12082 )
12083 .await
12084 .unwrap();
12085 insta::assert_snapshot!("test_add_fixed_owned_point", src_delta.text.as_str());
12086 assert_eq!(
12087 scene_delta.new_graph.objects.len(),
12088 6,
12089 "{:#?}",
12090 scene_delta.new_graph.objects
12091 );
12092
12093 ctx.close().await;
12094 mock_ctx.close().await;
12095 }
12096
12097 #[tokio::test(flavor = "multi_thread")]
12098 async fn test_radius_error_cases() {
12099 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12100 let mock_ctx = ExecutorContext::new_mock(None).await;
12101 let version = Version(0);
12102
12103 let initial_source_point = "\
12105sketch(on = XY) {
12106 point(at = [var 1, var 2])
12107}
12108";
12109 let program_point = Program::parse(initial_source_point).unwrap().0.unwrap();
12110 let mut frontend_point = FrontendState::new();
12111 frontend_point.hack_set_program(&ctx, program_point).await.unwrap();
12112 let sketch_object_point = find_first_sketch_object(&frontend_point.scene_graph).unwrap();
12113 let sketch_id_point = sketch_object_point.id;
12114 let sketch_point = expect_sketch(sketch_object_point);
12115 let point_id = *sketch_point.segments.first().unwrap();
12116
12117 let constraint_point = Constraint::Radius(Radius {
12118 arc: point_id,
12119 radius: Number {
12120 value: 5.0,
12121 units: NumericSuffix::Mm,
12122 },
12123 label_position: None,
12124 source: Default::default(),
12125 });
12126 let result_point = frontend_point
12127 .add_constraint(&mock_ctx, version, sketch_id_point, constraint_point)
12128 .await;
12129 assert!(result_point.is_err(), "Single point should error for radius");
12130
12131 let initial_source_line = "\
12133sketch(on = XY) {
12134 line(start = [var 1, var 2], end = [var 3, var 4])
12135}
12136";
12137 let program_line = Program::parse(initial_source_line).unwrap().0.unwrap();
12138 let mut frontend_line = FrontendState::new();
12139 frontend_line.hack_set_program(&ctx, program_line).await.unwrap();
12140 let sketch_object_line = find_first_sketch_object(&frontend_line.scene_graph).unwrap();
12141 let sketch_id_line = sketch_object_line.id;
12142 let sketch_line = expect_sketch(sketch_object_line);
12143 let line_id = *sketch_line.segments.first().unwrap();
12144
12145 let constraint_line = Constraint::Radius(Radius {
12146 arc: line_id,
12147 radius: Number {
12148 value: 5.0,
12149 units: NumericSuffix::Mm,
12150 },
12151 label_position: None,
12152 source: Default::default(),
12153 });
12154 let result_line = frontend_line
12155 .add_constraint(&mock_ctx, version, sketch_id_line, constraint_line)
12156 .await;
12157 assert!(result_line.is_err(), "Single line segment should error for radius");
12158
12159 ctx.close().await;
12160 mock_ctx.close().await;
12161 }
12162
12163 #[tokio::test(flavor = "multi_thread")]
12164 async fn test_diameter_single_arc_segment() {
12165 let initial_source = "\
12166sketch(on = XY) {
12167 arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
12168}
12169";
12170
12171 let program = Program::parse(initial_source).unwrap().0.unwrap();
12172
12173 let mut frontend = FrontendState::new();
12174
12175 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12176 let mock_ctx = ExecutorContext::new_mock(None).await;
12177 let version = Version(0);
12178
12179 frontend.hack_set_program(&ctx, program).await.unwrap();
12180 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12181 let sketch_id = sketch_object.id;
12182 let sketch = expect_sketch(sketch_object);
12183 let arc_id = sketch
12185 .segments
12186 .iter()
12187 .find(|&seg_id| {
12188 let obj = frontend.scene_graph.objects.get(seg_id.0);
12189 matches!(
12190 obj.map(|o| &o.kind),
12191 Some(ObjectKind::Segment {
12192 segment: Segment::Arc(_)
12193 })
12194 )
12195 })
12196 .unwrap();
12197
12198 let constraint = Constraint::Diameter(Diameter {
12199 arc: *arc_id,
12200 diameter: Number {
12201 value: 10.0,
12202 units: NumericSuffix::Mm,
12203 },
12204 label_position: None,
12205 source: Default::default(),
12206 });
12207 let (src_delta, scene_delta) = frontend
12208 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12209 .await
12210 .unwrap();
12211 insta::assert_snapshot!("test_diameter_single_arc_segment", src_delta.text.as_str());
12212 assert_eq!(
12213 scene_delta.new_graph.objects.len(),
12214 7, "{:#?}",
12216 scene_delta.new_graph.objects
12217 );
12218
12219 ctx.close().await;
12220 mock_ctx.close().await;
12221 }
12222
12223 #[tokio::test(flavor = "multi_thread")]
12224 async fn test_diameter_single_arc_segment_with_label_position() {
12225 let initial_source = "\
12226sketch(on = XY) {
12227 arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
12228}
12229";
12230
12231 let program = Program::parse(initial_source).unwrap().0.unwrap();
12232 let mut frontend = FrontendState::new();
12233 let mock_ctx = ExecutorContext::new_mock(None).await;
12234 let version = Version(0);
12235
12236 frontend.program = program.clone();
12237 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12238 frontend.update_state_after_exec(outcome, true);
12239 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12240 let sketch_id = sketch_object.id;
12241 let sketch = expect_sketch(sketch_object);
12242 let arc_id = sketch
12243 .segments
12244 .iter()
12245 .find(|&seg_id| {
12246 let obj = frontend.scene_graph.objects.get(seg_id.0);
12247 matches!(
12248 obj.map(|o| &o.kind),
12249 Some(ObjectKind::Segment {
12250 segment: Segment::Arc(_)
12251 })
12252 )
12253 })
12254 .unwrap();
12255
12256 let label_position = Point2d {
12257 x: Number {
12258 value: 10.0,
12259 units: NumericSuffix::Mm,
12260 },
12261 y: Number {
12262 value: 11.0,
12263 units: NumericSuffix::Mm,
12264 },
12265 };
12266 let constraint = Constraint::Diameter(Diameter {
12267 arc: *arc_id,
12268 diameter: Number {
12269 value: 10.0,
12270 units: NumericSuffix::Mm,
12271 },
12272 label_position: Some(label_position.clone()),
12273 source: Default::default(),
12274 });
12275 let (src_delta, scene_delta) = frontend
12276 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12277 .await
12278 .unwrap();
12279 insta::assert_snapshot!(
12280 "test_diameter_single_arc_segment_with_label_position",
12281 src_delta.text.as_str()
12282 );
12283
12284 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
12285 let sketch = expect_sketch(sketch_object);
12286 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
12287 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12288 panic!("Expected constraint object");
12289 };
12290 let Constraint::Diameter(diameter) = constraint else {
12291 panic!("Expected diameter constraint");
12292 };
12293 assert_eq!(diameter.label_position, Some(label_position));
12294
12295 mock_ctx.close().await;
12296 }
12297
12298 #[tokio::test(flavor = "multi_thread")]
12299 async fn test_edit_diameter_constraint_label_position() {
12300 let initial_source = "\
12301sketch(on = XY) {
12302 arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
12303 diameter(arc1) == 10mm
12304}
12305";
12306
12307 let program = Program::parse(initial_source).unwrap().0.unwrap();
12308 let mut frontend = FrontendState::new();
12309 let mock_ctx = ExecutorContext::new_mock(None).await;
12310 let version = Version(0);
12311
12312 frontend.program = program.clone();
12313 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12314 frontend.update_state_after_exec(outcome, true);
12315 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12316 let sketch_id = sketch_object.id;
12317 let sketch = expect_sketch(sketch_object);
12318 let constraint_id = sketch.constraints[0];
12319 let label_position = Point2d {
12320 x: Number {
12321 value: 10.0,
12322 units: NumericSuffix::Mm,
12323 },
12324 y: Number {
12325 value: 11.0,
12326 units: NumericSuffix::Mm,
12327 },
12328 };
12329
12330 let (src_delta, scene_delta) = frontend
12331 .edit_distance_constraint_label_position(
12332 &mock_ctx,
12333 version,
12334 sketch_id,
12335 constraint_id,
12336 label_position.clone(),
12337 vec![],
12338 )
12339 .await
12340 .unwrap();
12341 insta::assert_snapshot!("test_edit_diameter_constraint_label_position", src_delta.text.as_str());
12342
12343 let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
12344 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12345 panic!("Expected constraint object");
12346 };
12347 let Constraint::Diameter(diameter) = constraint else {
12348 panic!("Expected diameter constraint");
12349 };
12350 assert_eq!(diameter.label_position, Some(label_position));
12351
12352 mock_ctx.close().await;
12353 }
12354
12355 #[tokio::test(flavor = "multi_thread")]
12356 async fn test_diameter_error_cases() {
12357 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12358 let mock_ctx = ExecutorContext::new_mock(None).await;
12359 let version = Version(0);
12360
12361 let initial_source_point = "\
12363sketch(on = XY) {
12364 point(at = [var 1, var 2])
12365}
12366";
12367 let program_point = Program::parse(initial_source_point).unwrap().0.unwrap();
12368 let mut frontend_point = FrontendState::new();
12369 frontend_point.hack_set_program(&ctx, program_point).await.unwrap();
12370 let sketch_object_point = find_first_sketch_object(&frontend_point.scene_graph).unwrap();
12371 let sketch_id_point = sketch_object_point.id;
12372 let sketch_point = expect_sketch(sketch_object_point);
12373 let point_id = *sketch_point.segments.first().unwrap();
12374
12375 let constraint_point = Constraint::Diameter(Diameter {
12376 arc: point_id,
12377 diameter: Number {
12378 value: 10.0,
12379 units: NumericSuffix::Mm,
12380 },
12381 label_position: None,
12382 source: Default::default(),
12383 });
12384 let result_point = frontend_point
12385 .add_constraint(&mock_ctx, version, sketch_id_point, constraint_point)
12386 .await;
12387 assert!(result_point.is_err(), "Single point should error for diameter");
12388
12389 let initial_source_line = "\
12391sketch(on = XY) {
12392 line(start = [var 1, var 2], end = [var 3, var 4])
12393}
12394";
12395 let program_line = Program::parse(initial_source_line).unwrap().0.unwrap();
12396 let mut frontend_line = FrontendState::new();
12397 frontend_line.hack_set_program(&ctx, program_line).await.unwrap();
12398 let sketch_object_line = find_first_sketch_object(&frontend_line.scene_graph).unwrap();
12399 let sketch_id_line = sketch_object_line.id;
12400 let sketch_line = expect_sketch(sketch_object_line);
12401 let line_id = *sketch_line.segments.first().unwrap();
12402
12403 let constraint_line = Constraint::Diameter(Diameter {
12404 arc: line_id,
12405 diameter: Number {
12406 value: 10.0,
12407 units: NumericSuffix::Mm,
12408 },
12409 label_position: None,
12410 source: Default::default(),
12411 });
12412 let result_line = frontend_line
12413 .add_constraint(&mock_ctx, version, sketch_id_line, constraint_line)
12414 .await;
12415 assert!(result_line.is_err(), "Single line segment should error for diameter");
12416
12417 ctx.close().await;
12418 mock_ctx.close().await;
12419 }
12420
12421 #[tokio::test(flavor = "multi_thread")]
12422 async fn test_line_horizontal() {
12423 let initial_source = "\
12424sketch(on = XY) {
12425 line(start = [var 1, var 2], end = [var 3, var 4])
12426}
12427";
12428
12429 let program = Program::parse(initial_source).unwrap().0.unwrap();
12430
12431 let mut frontend = FrontendState::new();
12432
12433 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12434 let mock_ctx = ExecutorContext::new_mock(None).await;
12435 let version = Version(0);
12436
12437 frontend.hack_set_program(&ctx, program).await.unwrap();
12438 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12439 let sketch_id = sketch_object.id;
12440 let sketch = expect_sketch(sketch_object);
12441 let line1_id = *sketch.segments.get(2).unwrap();
12442
12443 let constraint = Constraint::Horizontal(Horizontal::Line { line: line1_id });
12444 let (src_delta, scene_delta) = frontend
12445 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12446 .await
12447 .unwrap();
12448 insta::assert_snapshot!("test_line_horizontal", src_delta.text.as_str());
12449 assert_eq!(
12450 scene_delta.new_graph.objects.len(),
12451 6,
12452 "{:#?}",
12453 scene_delta.new_graph.objects
12454 );
12455
12456 ctx.close().await;
12457 mock_ctx.close().await;
12458 }
12459
12460 #[tokio::test(flavor = "multi_thread")]
12461 async fn test_control_point_spline_edge_horizontal() {
12462 let initial_source = "\
12463@settings(experimentalFeatures = allow)
12464splineSketch = sketch(on = XY) {
12465 controlPointSpline1 = controlPointSpline(points = [
12466 [var 0mm, var 0mm],
12467 [var 10mm, var 20mm],
12468 [var 20mm, var 0mm],
12469 ])
12470}
12471";
12472
12473 let program = Program::parse(initial_source).unwrap().0.unwrap();
12474
12475 let mut frontend = FrontendState::new();
12476
12477 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12478 let mock_ctx = ExecutorContext::new_mock(None).await;
12479 let version = Version(0);
12480
12481 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
12482 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12483 let sketch_id = sketch_object.id;
12484 let sketch = expect_sketch(sketch_object);
12485 let spline_id = sketch
12486 .segments
12487 .iter()
12488 .copied()
12489 .find(|seg_id| {
12490 matches!(
12491 &frontend.scene_graph.objects[seg_id.0].kind,
12492 ObjectKind::Segment {
12493 segment: Segment::ControlPointSpline(_)
12494 }
12495 )
12496 })
12497 .expect("Expected a control point spline segment in sketch");
12498 let edge_id = frontend
12499 .scene_graph
12500 .objects
12501 .iter()
12502 .find_map(|obj| match &obj.kind {
12503 ObjectKind::Segment {
12504 segment: Segment::Line(line),
12505 } if line.owner == Some(spline_id) => Some(obj.id),
12506 _ => None,
12507 })
12508 .expect("Expected an owned control-polygon edge");
12509
12510 let constraint = Constraint::Horizontal(Horizontal::Line { line: edge_id });
12511 let (src_delta, _) = frontend
12512 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12513 .await
12514 .unwrap();
12515 assert!(
12516 src_delta.text.contains("horizontal(controlPointSpline1.edges[0])"),
12517 "Expected horizontal constraint on spline edge, got: {}",
12518 src_delta.text
12519 );
12520
12521 ctx.close().await;
12522 mock_ctx.close().await;
12523 }
12524
12525 #[tokio::test(flavor = "multi_thread")]
12526 async fn test_control_point_spline_edge_angle() {
12527 let initial_source = "\
12528@settings(experimentalFeatures = allow)
12529splineSketch = sketch(on = XY) {
12530 controlPointSpline1 = controlPointSpline(points = [
12531 [var 0mm, var 0mm],
12532 [var 10mm, var 20mm],
12533 [var 20mm, var 0mm],
12534 ])
12535
12536 line1 = line(start = [var 40mm, var 0mm], end = [var 60mm, var 10mm])
12537}
12538";
12539
12540 let program = Program::parse(initial_source).unwrap().0.unwrap();
12541
12542 let mut frontend = FrontendState::new();
12543
12544 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12545 let mock_ctx = ExecutorContext::new_mock(None).await;
12546 let version = Version(0);
12547
12548 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
12549 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12550 let sketch_id = sketch_object.id;
12551 let sketch = expect_sketch(sketch_object);
12552 let spline_id = sketch
12553 .segments
12554 .iter()
12555 .copied()
12556 .find(|seg_id| {
12557 matches!(
12558 &frontend.scene_graph.objects[seg_id.0].kind,
12559 ObjectKind::Segment {
12560 segment: Segment::ControlPointSpline(_)
12561 }
12562 )
12563 })
12564 .expect("Expected a control point spline segment in sketch");
12565 let edge_id = frontend
12566 .scene_graph
12567 .objects
12568 .iter()
12569 .find_map(|obj| match &obj.kind {
12570 ObjectKind::Segment {
12571 segment: Segment::Line(line),
12572 } if line.owner == Some(spline_id) => Some(obj.id),
12573 _ => None,
12574 })
12575 .expect("Expected an owned control-polygon edge");
12576 let line1_id = frontend
12577 .scene_graph
12578 .objects
12579 .iter()
12580 .find_map(|obj| match &obj.kind {
12581 ObjectKind::Segment {
12582 segment: Segment::Line(line),
12583 } if line.owner.is_none() && obj.label == "line1" => Some(obj.id),
12584 _ => None,
12585 })
12586 .or_else(|| {
12587 sketch.segments.iter().copied().find(|seg_id| {
12588 matches!(
12589 &frontend.scene_graph.objects[seg_id.0].kind,
12590 ObjectKind::Segment {
12591 segment: Segment::Line(line),
12592 } if line.owner.is_none()
12593 )
12594 })
12595 })
12596 .expect("Expected a standalone line segment in sketch");
12597
12598 let constraint = Constraint::Angle(Angle {
12599 lines: vec![line1_id, edge_id],
12600 angle: Number {
12601 value: 30.0,
12602 units: NumericSuffix::Deg,
12603 },
12604 source: Default::default(),
12605 });
12606 let (src_delta, _) = frontend
12607 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12608 .await
12609 .unwrap();
12610 assert!(
12611 src_delta
12612 .text
12613 .contains("angle([line1, controlPointSpline1.edges[0]]) == 30deg"),
12614 "Expected angle constraint on spline edge, got: {}",
12615 src_delta.text
12616 );
12617
12618 ctx.close().await;
12619 mock_ctx.close().await;
12620 }
12621
12622 #[tokio::test(flavor = "multi_thread")]
12623 async fn test_ui_scene_graph_hides_same_spline_coincident_constraints() {
12624 let initial_source = "\
12625@settings(experimentalFeatures = allow)
12626splineSketch = sketch(on = XY) {
12627 spline1 = controlPointSpline(points = [
12628 [var 0mm, var 0mm],
12629 [var 10mm, var 20mm],
12630 [var 20mm, var 0mm],
12631 ])
12632 line1 = line(start = [var 0mm, var 0mm], end = [var -10mm, var 0mm])
12633 coincident([spline1.controls[1], spline1.edges[0]])
12634 coincident([spline1.controls[0], line1])
12635}
12636";
12637
12638 let program = Program::parse(initial_source).unwrap().0.unwrap();
12639
12640 let mut frontend = FrontendState::new();
12641
12642 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12643 let mock_ctx = ExecutorContext::new_mock(None).await;
12644
12645 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
12646
12647 let ui_scene_graph = frontend.scene_graph_for_ui();
12648 let sketch_object = find_first_sketch_object(&ui_scene_graph).unwrap();
12649 let sketch = expect_sketch(sketch_object);
12650
12651 assert_eq!(
12652 sketch.constraints.len(),
12653 1,
12654 "Expected only the external coincident constraint to remain visible in the UI scene graph"
12655 );
12656
12657 let visible_constraints = ui_scene_graph
12658 .objects
12659 .iter()
12660 .filter_map(|object| match &object.kind {
12661 ObjectKind::Constraint {
12662 constraint: Constraint::Coincident(coincident),
12663 } => Some(coincident.clone()),
12664 _ => None,
12665 })
12666 .collect::<Vec<_>>();
12667
12668 assert_eq!(
12669 visible_constraints.len(),
12670 1,
12671 "Expected only one coincident constraint object in the UI scene graph"
12672 );
12673 assert_eq!(
12674 visible_constraints[0].get_segments().len(),
12675 2,
12676 "Expected the remaining visible coincident constraint to reference two segments"
12677 );
12678
12679 ctx.close().await;
12680 mock_ctx.close().await;
12681 }
12682
12683 #[tokio::test(flavor = "multi_thread")]
12684 async fn test_edit_control_point_spline_can_append_control_point() {
12685 let initial_source = "\
12686@settings(experimentalFeatures = allow)
12687splineSketch = sketch(on = XY) {
12688 controlPointSpline(points = [
12689 [var 0mm, var 0mm],
12690 [var 10mm, var 20mm],
12691 [var 20mm, var 0mm],
12692 ])
12693}
12694";
12695
12696 let program = Program::parse(initial_source).unwrap().0.unwrap();
12697
12698 let mut frontend = FrontendState::new();
12699
12700 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12701 let mock_ctx = ExecutorContext::new_mock(None).await;
12702 let version = Version(0);
12703
12704 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
12705 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12706 let sketch_id = sketch_object.id;
12707 let sketch = expect_sketch(sketch_object);
12708 let spline_id = sketch
12709 .segments
12710 .iter()
12711 .copied()
12712 .find(|seg_id| {
12713 matches!(
12714 &frontend.scene_graph.objects[seg_id.0].kind,
12715 ObjectKind::Segment {
12716 segment: Segment::ControlPointSpline(_)
12717 }
12718 )
12719 })
12720 .expect("Expected a control point spline segment in sketch");
12721
12722 let ctor = ControlPointSplineCtor {
12723 points: vec![
12724 Point2d {
12725 x: Expr::Var(Number {
12726 value: 0.0,
12727 units: NumericSuffix::Mm,
12728 }),
12729 y: Expr::Var(Number {
12730 value: 0.0,
12731 units: NumericSuffix::Mm,
12732 }),
12733 },
12734 Point2d {
12735 x: Expr::Var(Number {
12736 value: 10.0,
12737 units: NumericSuffix::Mm,
12738 }),
12739 y: Expr::Var(Number {
12740 value: 20.0,
12741 units: NumericSuffix::Mm,
12742 }),
12743 },
12744 Point2d {
12745 x: Expr::Var(Number {
12746 value: 20.0,
12747 units: NumericSuffix::Mm,
12748 }),
12749 y: Expr::Var(Number {
12750 value: 0.0,
12751 units: NumericSuffix::Mm,
12752 }),
12753 },
12754 Point2d {
12755 x: Expr::Var(Number {
12756 value: 30.0,
12757 units: NumericSuffix::Mm,
12758 }),
12759 y: Expr::Var(Number {
12760 value: 10.0,
12761 units: NumericSuffix::Mm,
12762 }),
12763 },
12764 ],
12765 construction: None,
12766 };
12767
12768 let segments = vec![ExistingSegmentCtor {
12769 id: spline_id,
12770 ctor: SegmentCtor::ControlPointSpline(ctor),
12771 }];
12772 let (src_delta, scene_delta) = frontend
12773 .edit_segments(&mock_ctx, version, sketch_id, segments)
12774 .await
12775 .unwrap();
12776
12777 assert!(
12778 src_delta.text.contains("[var 30mm, var 10mm]"),
12779 "Expected appended spline control point in source, got: {}",
12780 src_delta.text
12781 );
12782
12783 assert!(
12784 scene_delta.invalidates_ids,
12785 "Expected appending a spline control point to invalidate ids"
12786 );
12787 let updated_spline = scene_delta
12788 .new_graph
12789 .objects
12790 .iter()
12791 .find_map(|obj| match &obj.kind {
12792 ObjectKind::Segment {
12793 segment: Segment::ControlPointSpline(updated_spline),
12794 } if updated_spline.controls.len() == 4 => Some(updated_spline),
12795 _ => None,
12796 })
12797 .expect("Expected edited scene graph to contain a four-point control point spline");
12798 assert_eq!(
12799 updated_spline.controls.len(),
12800 4,
12801 "Expected edited spline to expose four control points"
12802 );
12803
12804 ctx.close().await;
12805 mock_ctx.close().await;
12806 }
12807
12808 #[tokio::test(flavor = "multi_thread")]
12809 async fn test_line_vertical() {
12810 let initial_source = "\
12811sketch(on = XY) {
12812 line(start = [var 1, var 2], end = [var 3, var 4])
12813}
12814";
12815
12816 let program = Program::parse(initial_source).unwrap().0.unwrap();
12817
12818 let mut frontend = FrontendState::new();
12819
12820 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12821 let mock_ctx = ExecutorContext::new_mock(None).await;
12822 let version = Version(0);
12823
12824 frontend.hack_set_program(&ctx, program).await.unwrap();
12825 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12826 let sketch_id = sketch_object.id;
12827 let sketch = expect_sketch(sketch_object);
12828 let line1_id = *sketch.segments.get(2).unwrap();
12829
12830 let constraint = Constraint::Vertical(Vertical::Line { line: line1_id });
12831 let (src_delta, scene_delta) = frontend
12832 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12833 .await
12834 .unwrap();
12835 insta::assert_snapshot!("test_line_vertical", src_delta.text.as_str());
12836 assert_eq!(
12837 scene_delta.new_graph.objects.len(),
12838 6,
12839 "{:#?}",
12840 scene_delta.new_graph.objects
12841 );
12842
12843 ctx.close().await;
12844 mock_ctx.close().await;
12845 }
12846
12847 #[tokio::test(flavor = "multi_thread")]
12848 async fn test_points_vertical() {
12849 let initial_source = "\
12850sketch001 = sketch(on = XY) {
12851 p0 = point(at = [var -2.23mm, var 3.1mm])
12852 pf = point(at = [4, 4])
12853}
12854";
12855
12856 let program = Program::parse(initial_source).unwrap().0.unwrap();
12857
12858 let mut frontend = FrontendState::new();
12859
12860 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12861 let mock_ctx = ExecutorContext::new_mock(None).await;
12862 let version = Version(0);
12863
12864 frontend.hack_set_program(&ctx, program).await.unwrap();
12865 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12866 let sketch_id = sketch_object.id;
12867 let sketch = expect_sketch(sketch_object);
12868 let point_ids = vec![
12869 sketch.segments.first().unwrap().to_owned(),
12870 sketch.segments.get(1).unwrap().to_owned(),
12871 ];
12872
12873 let constraint = Constraint::Vertical(Vertical::Points {
12874 points: point_ids.into_iter().map(ConstraintSegment::from).collect(),
12875 });
12876 let (src_delta, scene_delta) = frontend
12877 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12878 .await
12879 .unwrap();
12880 insta::assert_snapshot!("test_points_vertical", src_delta.text.as_str());
12881 assert_eq!(
12882 scene_delta.new_graph.objects.len(),
12883 5,
12884 "{:#?}",
12885 scene_delta.new_graph.objects
12886 );
12887
12888 ctx.close().await;
12889 mock_ctx.close().await;
12890 }
12891
12892 #[tokio::test(flavor = "multi_thread")]
12893 async fn test_points_horizontal() {
12894 let initial_source = "\
12895sketch001 = sketch(on = XY) {
12896 p0 = point(at = [var -2.23mm, var 3.1mm])
12897 pf = point(at = [4, 4])
12898}
12899";
12900
12901 let program = Program::parse(initial_source).unwrap().0.unwrap();
12902
12903 let mut frontend = FrontendState::new();
12904
12905 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12906 let mock_ctx = ExecutorContext::new_mock(None).await;
12907 let version = Version(0);
12908
12909 frontend.hack_set_program(&ctx, program).await.unwrap();
12910 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12911 let sketch_id = sketch_object.id;
12912 let sketch = expect_sketch(sketch_object);
12913 let point_ids = vec![
12914 sketch.segments.first().unwrap().to_owned(),
12915 sketch.segments.get(1).unwrap().to_owned(),
12916 ];
12917
12918 let constraint = Constraint::Horizontal(Horizontal::Points {
12919 points: point_ids.into_iter().map(ConstraintSegment::from).collect(),
12920 });
12921 let (src_delta, scene_delta) = frontend
12922 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12923 .await
12924 .unwrap();
12925 insta::assert_snapshot!("test_points_horizontal", src_delta.text.as_str());
12926 assert_eq!(
12927 scene_delta.new_graph.objects.len(),
12928 5,
12929 "{:#?}",
12930 scene_delta.new_graph.objects
12931 );
12932
12933 ctx.close().await;
12934 mock_ctx.close().await;
12935 }
12936
12937 #[tokio::test(flavor = "multi_thread")]
12938 async fn test_point_horizontal_with_origin() {
12939 let initial_source = "\
12940sketch001 = sketch(on = XY) {
12941 p0 = point(at = [var -2.23mm, var 3.1mm])
12942}
12943";
12944
12945 let program = Program::parse(initial_source).unwrap().0.unwrap();
12946
12947 let mut frontend = FrontendState::new();
12948
12949 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12950 let mock_ctx = ExecutorContext::new_mock(None).await;
12951 let version = Version(0);
12952
12953 frontend.hack_set_program(&ctx, program).await.unwrap();
12954 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12955 let sketch_id = sketch_object.id;
12956 let sketch = expect_sketch(sketch_object);
12957 let point_id = *sketch.segments.first().unwrap();
12958
12959 let constraint = Constraint::Horizontal(Horizontal::Points {
12960 points: vec![ConstraintSegment::from(point_id), ConstraintSegment::ORIGIN],
12961 });
12962 let (src_delta, scene_delta) = frontend
12963 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12964 .await
12965 .unwrap();
12966 insta::assert_snapshot!("test_point_horizontal_with_origin", src_delta.text.as_str());
12967 assert_eq!(
12968 scene_delta.new_graph.objects.len(),
12969 4,
12970 "{:#?}",
12971 scene_delta.new_graph.objects
12972 );
12973
12974 ctx.close().await;
12975 mock_ctx.close().await;
12976 }
12977
12978 #[tokio::test(flavor = "multi_thread")]
12979 async fn test_lines_equal_length() {
12980 let initial_source = "\
12981sketch(on = XY) {
12982 line(start = [var 1, var 2], end = [var 3, var 4])
12983 line(start = [var 5, var 6], end = [var 7, var 8])
12984}
12985";
12986
12987 let program = Program::parse(initial_source).unwrap().0.unwrap();
12988
12989 let mut frontend = FrontendState::new();
12990
12991 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12992 let mock_ctx = ExecutorContext::new_mock(None).await;
12993 let version = Version(0);
12994
12995 frontend.hack_set_program(&ctx, program).await.unwrap();
12996 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12997 let sketch_id = sketch_object.id;
12998 let sketch = expect_sketch(sketch_object);
12999 let line1_id = *sketch.segments.get(2).unwrap();
13000 let line2_id = *sketch.segments.get(5).unwrap();
13001
13002 let constraint = Constraint::LinesEqualLength(LinesEqualLength {
13003 lines: vec![line1_id, line2_id],
13004 });
13005 let (src_delta, scene_delta) = frontend
13006 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13007 .await
13008 .unwrap();
13009 insta::assert_snapshot!("test_lines_equal_length", src_delta.text.as_str());
13010 assert_eq!(
13011 scene_delta.new_graph.objects.len(),
13012 9,
13013 "{:#?}",
13014 scene_delta.new_graph.objects
13015 );
13016
13017 ctx.close().await;
13018 mock_ctx.close().await;
13019 }
13020
13021 #[tokio::test(flavor = "multi_thread")]
13022 async fn test_add_constraint_multi_line_equal_length() {
13023 let initial_source = "\
13024sketch(on = XY) {
13025 line(start = [var 1, var 2], end = [var 3, var 4])
13026 line(start = [var 5, var 6], end = [var 7, var 8])
13027 line(start = [var 9, var 10], end = [var 11, var 12])
13028}
13029";
13030
13031 let program = Program::parse(initial_source).unwrap().0.unwrap();
13032
13033 let mut frontend = FrontendState::new();
13034 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13035 let mock_ctx = ExecutorContext::new_mock(None).await;
13036 let version = Version(0);
13037
13038 frontend.hack_set_program(&ctx, program).await.unwrap();
13039 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13040 let sketch_id = sketch_object.id;
13041 let sketch = expect_sketch(sketch_object);
13042 let line1_id = *sketch.segments.get(2).unwrap();
13043 let line2_id = *sketch.segments.get(5).unwrap();
13044 let line3_id = *sketch.segments.get(8).unwrap();
13045
13046 let constraint = Constraint::LinesEqualLength(LinesEqualLength {
13047 lines: vec![line1_id, line2_id, line3_id],
13048 });
13049 let (src_delta, scene_delta) = frontend
13050 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13051 .await
13052 .unwrap();
13053 insta::assert_snapshot!("test_add_constraint_multi_line_equal_length", src_delta.text.as_str());
13054 let constraints = scene_delta
13055 .new_graph
13056 .objects
13057 .iter()
13058 .filter_map(|obj| {
13059 let ObjectKind::Constraint { constraint } = &obj.kind else {
13060 return None;
13061 };
13062 Some(constraint)
13063 })
13064 .collect::<Vec<_>>();
13065
13066 assert_eq!(constraints.len(), 1, "{:#?}", frontend.scene_graph.objects);
13067 let Constraint::LinesEqualLength(lines_equal_length) = constraints[0] else {
13068 panic!("expected equal length constraint, got {:?}", constraints[0]);
13069 };
13070 assert_eq!(lines_equal_length.lines.len(), 3);
13071
13072 ctx.close().await;
13073 mock_ctx.close().await;
13074 }
13075
13076 #[tokio::test(flavor = "multi_thread")]
13077 async fn test_lines_parallel() {
13078 let initial_source = "\
13079sketch(on = XY) {
13080 line(start = [var 1, var 2], end = [var 3, var 4])
13081 line(start = [var 5, var 6], end = [var 7, var 8])
13082}
13083";
13084
13085 let program = Program::parse(initial_source).unwrap().0.unwrap();
13086
13087 let mut frontend = FrontendState::new();
13088
13089 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13090 let mock_ctx = ExecutorContext::new_mock(None).await;
13091 let version = Version(0);
13092
13093 frontend.hack_set_program(&ctx, program).await.unwrap();
13094 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13095 let sketch_id = sketch_object.id;
13096 let sketch = expect_sketch(sketch_object);
13097 let line1_id = *sketch.segments.get(2).unwrap();
13098 let line2_id = *sketch.segments.get(5).unwrap();
13099
13100 let constraint = Constraint::Parallel(Parallel {
13101 lines: vec![line1_id, line2_id],
13102 });
13103 let (src_delta, scene_delta) = frontend
13104 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13105 .await
13106 .unwrap();
13107 insta::assert_snapshot!("test_lines_parallel", src_delta.text.as_str());
13108 assert_eq!(
13109 scene_delta.new_graph.objects.len(),
13110 9,
13111 "{:#?}",
13112 scene_delta.new_graph.objects
13113 );
13114
13115 ctx.close().await;
13116 mock_ctx.close().await;
13117 }
13118
13119 #[tokio::test(flavor = "multi_thread")]
13120 async fn test_lines_parallel_multiline() {
13121 let initial_source = "\
13122sketch(on = XY) {
13123 line(start = [var 1, var 2], end = [var 3, var 4])
13124 line(start = [var 5, var 6], end = [var 7, var 8])
13125 line(start = [var 9, var 10], end = [var 11, var 12])
13126}
13127";
13128
13129 let program = Program::parse(initial_source).unwrap().0.unwrap();
13130
13131 let mut frontend = FrontendState::new();
13132
13133 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13134 let mock_ctx = ExecutorContext::new_mock(None).await;
13135 let version = Version(0);
13136
13137 frontend.hack_set_program(&ctx, program).await.unwrap();
13138 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13139 let sketch_id = sketch_object.id;
13140 let sketch = expect_sketch(sketch_object);
13141 let line1_id = *sketch.segments.get(2).unwrap();
13142 let line2_id = *sketch.segments.get(5).unwrap();
13143 let line3_id = *sketch.segments.get(8).unwrap();
13144
13145 let constraint = Constraint::Parallel(Parallel {
13146 lines: vec![line1_id, line2_id, line3_id],
13147 });
13148 let (src_delta, scene_delta) = frontend
13149 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13150 .await
13151 .unwrap();
13152 insta::assert_snapshot!("test_lines_parallel_multiline", src_delta.text.as_str());
13153
13154 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
13155 let sketch = expect_sketch(sketch_object);
13156 assert_eq!(sketch.constraints.len(), 1);
13157
13158 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
13159 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
13160 panic!("Expected constraint object");
13161 };
13162 let Constraint::Parallel(parallel) = constraint else {
13163 panic!("Expected parallel constraint");
13164 };
13165 assert_eq!(parallel.lines.len(), 3);
13166
13167 ctx.close().await;
13168 mock_ctx.close().await;
13169 }
13170
13171 #[tokio::test(flavor = "multi_thread")]
13172 async fn test_lines_perpendicular() {
13173 let initial_source = "\
13174sketch(on = XY) {
13175 line(start = [var 1, var 2], end = [var 3, var 4])
13176 line(start = [var 5, var 6], end = [var 7, var 8])
13177}
13178";
13179
13180 let program = Program::parse(initial_source).unwrap().0.unwrap();
13181
13182 let mut frontend = FrontendState::new();
13183
13184 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13185 let mock_ctx = ExecutorContext::new_mock(None).await;
13186 let version = Version(0);
13187
13188 frontend.hack_set_program(&ctx, program).await.unwrap();
13189 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13190 let sketch_id = sketch_object.id;
13191 let sketch = expect_sketch(sketch_object);
13192 let line1_id = *sketch.segments.get(2).unwrap();
13193 let line2_id = *sketch.segments.get(5).unwrap();
13194
13195 let constraint = Constraint::Perpendicular(Perpendicular {
13196 lines: vec![line1_id, line2_id],
13197 });
13198 let (src_delta, scene_delta) = frontend
13199 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13200 .await
13201 .unwrap();
13202 insta::assert_snapshot!("test_lines_perpendicular", src_delta.text.as_str());
13203 assert_eq!(
13204 scene_delta.new_graph.objects.len(),
13205 9,
13206 "{:#?}",
13207 scene_delta.new_graph.objects
13208 );
13209
13210 ctx.close().await;
13211 mock_ctx.close().await;
13212 }
13213
13214 #[tokio::test(flavor = "multi_thread")]
13215 async fn test_lines_angle() {
13216 let initial_source = "\
13217sketch(on = XY) {
13218 line(start = [var 1, var 2], end = [var 3, var 4])
13219 line(start = [var 5, var 6], end = [var 7, var 8])
13220}
13221";
13222
13223 let program = Program::parse(initial_source).unwrap().0.unwrap();
13224
13225 let mut frontend = FrontendState::new();
13226
13227 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13228 let mock_ctx = ExecutorContext::new_mock(None).await;
13229 let version = Version(0);
13230
13231 frontend.hack_set_program(&ctx, program).await.unwrap();
13232 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13233 let sketch_id = sketch_object.id;
13234 let sketch = expect_sketch(sketch_object);
13235 let line1_id = *sketch.segments.get(2).unwrap();
13236 let line2_id = *sketch.segments.get(5).unwrap();
13237
13238 let constraint = Constraint::Angle(Angle {
13239 lines: vec![line1_id, line2_id],
13240 angle: Number {
13241 value: 30.0,
13242 units: NumericSuffix::Deg,
13243 },
13244 source: Default::default(),
13245 });
13246 let (src_delta, scene_delta) = frontend
13247 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13248 .await
13249 .unwrap();
13250 insta::assert_snapshot!("test_lines_angle", src_delta.text.as_str());
13251 assert_eq!(
13252 scene_delta.new_graph.objects.len(),
13253 9,
13254 "{:#?}",
13255 scene_delta.new_graph.objects
13256 );
13257
13258 ctx.close().await;
13259 mock_ctx.close().await;
13260 }
13261
13262 #[tokio::test(flavor = "multi_thread")]
13263 async fn test_segments_tangent() {
13264 let initial_source = "\
13265sketch(on = XY) {
13266 line(start = [var 1, var 2], end = [var 3, var 4])
13267 arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
13268}
13269";
13270
13271 let program = Program::parse(initial_source).unwrap().0.unwrap();
13272
13273 let mut frontend = FrontendState::new();
13274
13275 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13276 let mock_ctx = ExecutorContext::new_mock(None).await;
13277 let version = Version(0);
13278
13279 frontend.hack_set_program(&ctx, program).await.unwrap();
13280 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13281 let sketch_id = sketch_object.id;
13282 let sketch = expect_sketch(sketch_object);
13283 let line1_id = *sketch.segments.get(2).unwrap();
13284 let arc1_id = *sketch.segments.get(6).unwrap();
13285
13286 let constraint = Constraint::Tangent(Tangent {
13287 input: vec![line1_id, arc1_id],
13288 });
13289 let (src_delta, scene_delta) = frontend
13290 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13291 .await
13292 .unwrap();
13293 insta::assert_snapshot!("test_segments_tangent", src_delta.text.as_str());
13294 assert_eq!(
13295 scene_delta.new_graph.objects.len(),
13296 10,
13297 "{:#?}",
13298 scene_delta.new_graph.objects
13299 );
13300
13301 ctx.close().await;
13302 mock_ctx.close().await;
13303 }
13304
13305 #[tokio::test(flavor = "multi_thread")]
13306 async fn test_point_midpoint() {
13307 let initial_source = "\
13308sketch(on = XY) {
13309 point(at = [var 1, var 1])
13310 line(start = [var 0, var 0], end = [var 6, var 4])
13311}
13312";
13313
13314 let program = Program::parse(initial_source).unwrap().0.unwrap();
13315
13316 let mut frontend = FrontendState::new();
13317
13318 let ctx = ExecutorContext::new_mock(None).await;
13319 let version = Version(0);
13320
13321 frontend.program = program.clone();
13322 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
13323 frontend.update_state_after_exec(outcome, true);
13324 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13325 let sketch_id = sketch_object.id;
13326 let sketch = expect_sketch(sketch_object);
13327 let point_id = *sketch.segments.first().unwrap();
13328 let line_id = *sketch.segments.get(3).unwrap();
13329
13330 let constraint = Constraint::Midpoint(Midpoint {
13331 point: ConstraintSegment::from(point_id),
13332 segment: line_id,
13333 });
13334 let (src_delta, scene_delta) = frontend
13335 .add_constraint(&ctx, version, sketch_id, constraint)
13336 .await
13337 .unwrap();
13338 insta::assert_snapshot!("test_point_midpoint", src_delta.text.as_str());
13339 assert_eq!(
13340 scene_delta.new_graph.objects.len(),
13341 7,
13342 "{:#?}",
13343 scene_delta.new_graph.objects
13344 );
13345
13346 ctx.close().await;
13347 }
13348
13349 #[tokio::test(flavor = "multi_thread")]
13350 async fn test_segments_symmetric() {
13351 let initial_source = "\
13352sketch(on = XY) {
13353 line(start = [var 0, var 0], end = [var 0, var 4])
13354 line(start = [var 4, var 0], end = [var 4, var 4])
13355 line(start = [var 2, var -1], end = [var 2, var 5])
13356}
13357";
13358
13359 let program = Program::parse(initial_source).unwrap().0.unwrap();
13360
13361 let mut frontend = FrontendState::new();
13362
13363 let ctx = ExecutorContext::new_mock(None).await;
13364 let version = Version(0);
13365
13366 frontend.program = program.clone();
13367 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
13368 frontend.update_state_after_exec(outcome, true);
13369 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13370 let sketch_id = sketch_object.id;
13371 let sketch = expect_sketch(sketch_object);
13372 let line1_id = *sketch.segments.get(2).unwrap();
13373 let line2_id = *sketch.segments.get(5).unwrap();
13374 let axis_id = *sketch.segments.get(8).unwrap();
13375
13376 let constraint = Constraint::Symmetric(Symmetric {
13377 input: vec![line1_id, line2_id],
13378 axis: axis_id,
13379 });
13380 let (src_delta, scene_delta) = frontend
13381 .add_constraint(&ctx, version, sketch_id, constraint)
13382 .await
13383 .unwrap();
13384 insta::assert_snapshot!("test_segments_symmetric", src_delta.text.as_str());
13385 assert_eq!(
13386 scene_delta.new_graph.objects.len(),
13387 12,
13388 "{:#?}",
13389 scene_delta.new_graph.objects
13390 );
13391
13392 ctx.close().await;
13393 }
13394
13395 #[tokio::test(flavor = "multi_thread")]
13396 async fn test_point_arc_midpoint() {
13397 let initial_source = "\
13398sketch(on = XY) {
13399 point(at = [var 6, var 3])
13400 arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
13401}
13402";
13403
13404 let program = Program::parse(initial_source).unwrap().0.unwrap();
13405
13406 let mut frontend = FrontendState::new();
13407
13408 let ctx = ExecutorContext::new_mock(None).await;
13409 let version = Version(0);
13410
13411 frontend.program = program.clone();
13412 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
13413 frontend.update_state_after_exec(outcome, true);
13414 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13415 let sketch_id = sketch_object.id;
13416 let sketch = expect_sketch(sketch_object);
13417 let point_id = *sketch.segments.first().unwrap();
13418 let arc_id = *sketch.segments.get(4).unwrap();
13419
13420 let constraint = Constraint::Midpoint(Midpoint {
13421 point: ConstraintSegment::from(point_id),
13422 segment: arc_id,
13423 });
13424 let (src_delta, scene_delta) = frontend
13425 .add_constraint(&ctx, version, sketch_id, constraint)
13426 .await
13427 .unwrap();
13428 insta::assert_snapshot!("test_point_arc_midpoint", src_delta.text.as_str());
13429 assert_eq!(
13430 scene_delta.new_graph.objects.len(),
13431 8,
13432 "{:#?}",
13433 scene_delta.new_graph.objects
13434 );
13435
13436 ctx.close().await;
13437 }
13438
13439 #[tokio::test(flavor = "multi_thread")]
13440 async fn test_origin_line_midpoint() {
13441 let initial_source = "\
13442sketch(on = XY) {
13443 line(start = [var 0, var 0], end = [var 6, var 4])
13444}
13445";
13446
13447 let program = Program::parse(initial_source).unwrap().0.unwrap();
13448
13449 let mut frontend = FrontendState::new();
13450
13451 let ctx = ExecutorContext::new_mock(None).await;
13452 let version = Version(0);
13453
13454 frontend.program = program.clone();
13455 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
13456 frontend.update_state_after_exec(outcome, true);
13457 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13458 let sketch_id = sketch_object.id;
13459 let sketch = expect_sketch(sketch_object);
13460 let line_id = *sketch.segments.get(2).unwrap();
13461
13462 let constraint = Constraint::Midpoint(Midpoint {
13463 point: ConstraintSegment::ORIGIN,
13464 segment: line_id,
13465 });
13466 let (src_delta, scene_delta) = frontend
13467 .add_constraint(&ctx, version, sketch_id, constraint)
13468 .await
13469 .unwrap();
13470 insta::assert_snapshot!("test_origin_line_midpoint", src_delta.text.as_str());
13471 assert_eq!(
13472 scene_delta.new_graph.objects.len(),
13473 6,
13474 "{:#?}",
13475 scene_delta.new_graph.objects
13476 );
13477
13478 ctx.close().await;
13479 }
13480
13481 #[tokio::test(flavor = "multi_thread")]
13482 async fn test_origin_arc_midpoint() {
13483 let initial_source = "\
13484sketch(on = XY) {
13485 arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
13486}
13487";
13488
13489 let program = Program::parse(initial_source).unwrap().0.unwrap();
13490
13491 let mut frontend = FrontendState::new();
13492
13493 let ctx = ExecutorContext::new_mock(None).await;
13494 let version = Version(0);
13495
13496 frontend.program = program.clone();
13497 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
13498 frontend.update_state_after_exec(outcome, true);
13499 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13500 let sketch_id = sketch_object.id;
13501 let sketch = expect_sketch(sketch_object);
13502 let arc_id = *sketch.segments.get(3).unwrap();
13503
13504 let constraint = Constraint::Midpoint(Midpoint {
13505 point: ConstraintSegment::ORIGIN,
13506 segment: arc_id,
13507 });
13508 let (src_delta, scene_delta) = frontend
13509 .add_constraint(&ctx, version, sketch_id, constraint)
13510 .await
13511 .unwrap();
13512 insta::assert_snapshot!("test_origin_arc_midpoint", src_delta.text.as_str());
13513 assert_eq!(
13514 scene_delta.new_graph.objects.len(),
13515 7,
13516 "{:#?}",
13517 scene_delta.new_graph.objects
13518 );
13519
13520 ctx.close().await;
13521 }
13522
13523 #[tokio::test(flavor = "multi_thread")]
13524 async fn test_segments_symmetric_arcs() {
13525 let initial_source = "\
13526sketch(on = XY) {
13527 arc(start = [var -15, var 0], end = [var -10, var 5], center = [var -10, var 0])
13528 arc(start = [var 6, var 2], end = [var 12, var -4], center = [var 8, var 1])
13529 line(start = [var 0, var -10], end = [var 0, var 10])
13530}
13531";
13532
13533 let program = Program::parse(initial_source).unwrap().0.unwrap();
13534
13535 let mut frontend = FrontendState::new();
13536
13537 let ctx = ExecutorContext::new_mock(None).await;
13538 let version = Version(0);
13539
13540 frontend.program = program.clone();
13541 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
13542 frontend.update_state_after_exec(outcome, true);
13543 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13544 let sketch_id = sketch_object.id;
13545 let sketch = expect_sketch(sketch_object);
13546 let arc1_id = *sketch.segments.get(3).unwrap();
13547 let arc2_id = *sketch.segments.get(7).unwrap();
13548 let axis_id = *sketch.segments.get(10).unwrap();
13549
13550 let constraint = Constraint::Symmetric(Symmetric {
13551 input: vec![arc1_id, arc2_id],
13552 axis: axis_id,
13553 });
13554 let (src_delta, scene_delta) = frontend
13555 .add_constraint(&ctx, version, sketch_id, constraint)
13556 .await
13557 .unwrap();
13558 insta::assert_snapshot!("test_segments_symmetric_arcs", src_delta.text.as_str());
13559 assert_eq!(
13560 scene_delta.new_graph.objects.len(),
13561 14,
13562 "{:#?}",
13563 scene_delta.new_graph.objects
13564 );
13565
13566 ctx.close().await;
13567 }
13568
13569 #[tokio::test(flavor = "multi_thread")]
13570 async fn test_sketch_on_face_simple() {
13571 let initial_source = "\
13572len = 2mm
13573cube = startSketchOn(XY)
13574 |> startProfile(at = [0, 0])
13575 |> line(end = [len, 0], tag = $side)
13576 |> line(end = [0, len])
13577 |> line(end = [-len, 0])
13578 |> line(end = [0, -len])
13579 |> close()
13580 |> extrude(length = len)
13581
13582face = faceOf(cube, face = side)
13583";
13584
13585 let program = Program::parse(initial_source).unwrap().0.unwrap();
13586
13587 let mut frontend = FrontendState::new();
13588
13589 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13590 let mock_ctx = ExecutorContext::new_mock(None).await;
13591 let version = Version(0);
13592
13593 frontend.hack_set_program(&ctx, program).await.unwrap();
13594 let face_object = find_first_face_object(&frontend.scene_graph).unwrap();
13595 let face_id = face_object.id;
13596
13597 let sketch_args = SketchCtor {
13598 on: Plane::Object(face_id),
13599 };
13600 let (_src_delta, scene_delta, sketch_id) = frontend
13601 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
13602 .await
13603 .unwrap();
13604 assert_eq!(sketch_id, ObjectId(2));
13605 assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
13606 let sketch_object = &scene_delta.new_graph.objects[2];
13607 assert_eq!(sketch_object.id, ObjectId(2));
13608 assert_eq!(
13609 sketch_object.kind,
13610 ObjectKind::Sketch(Sketch {
13611 args: SketchCtor {
13612 on: Plane::Object(face_id),
13613 },
13614 plane: face_id,
13615 segments: vec![],
13616 constraints: vec![],
13617 })
13618 );
13619 assert_eq!(scene_delta.new_graph.objects.len(), 8);
13620
13621 ctx.close().await;
13622 mock_ctx.close().await;
13623 }
13624
13625 #[tokio::test(flavor = "multi_thread")]
13626 async fn test_new_sketch_on_primitive_index_face() {
13627 let initial_source = "\
13628@settings(kclVersion = 2.0)
13629
13630sketch001 = sketch(on = XY) {
13631 circle1 = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
13632}
13633extrude001 = extrude(region(point = [0mm, 0mm], sketch = sketch001), length = 5, tagEnd = $capEnd001)
13634shell001 = shell(extrude001, faces = capEnd001, thickness = 1)";
13635 let program = Program::parse(initial_source).unwrap().0.unwrap();
13636 let ctx = ExecutorContext::new_mock(None).await;
13637 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
13638 let solid_id = match outcome.variables.get("shell001") {
13639 Some(KclValueView::Solid { value }) => value.id,
13640 value => panic!("expected shell001 to be a solid, got {value:?}"),
13641 };
13642 let solid_references = solid_references_from_variables(&program.ast, &outcome.variables);
13643
13644 let mut ast = program.ast;
13645 let scene_graph = SceneGraph::empty(ProjectId(0), FileId(0), Version(0));
13646 let face_expr = sketch_on_ast_expr(
13647 &mut ast,
13648 &scene_graph,
13649 &solid_references,
13650 &Plane::PrimitiveFace(crate::frontend::api::PrimitiveFacePlane { solid_id, index: 6 }),
13651 )
13652 .unwrap();
13653 let face_decl = ast::VariableDeclaration::new(
13654 ast::VariableDeclarator::new("face001", face_expr),
13655 ast::ItemVisibility::Default,
13656 ast::VariableKind::Const,
13657 );
13658 ast.body
13659 .push(ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(
13660 face_decl,
13661 ))));
13662 let face_source = source_from_ast(&ast);
13663 let new_source = format!("{face_source}sketch002 = sketch(on = face001) {{\n}}\n");
13664 insta::assert_snapshot!("test_new_sketch_on_primitive_index_face", new_source);
13665
13666 let program = Program::parse(&new_source).unwrap().0.unwrap();
13667 ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
13668 ctx.close().await;
13669 }
13670
13671 #[tokio::test(flavor = "multi_thread")]
13672 async fn test_sketch_on_wall_artifact_from_region_extrude() {
13673 let initial_source = "\
13674s = sketch(on = YZ) {
13675 line1 = line(start = [0, 0], end = [0, 1])
13676 line2 = line(start = [0, 1], end = [1, 1])
13677 line3 = line(start = [1, 1], end = [0, 0])
13678}
13679region001 = region(point = [0.1, 0.1], sketch = s)
13680extrude001 = extrude(region001, length = 5)
13681";
13682
13683 let program = Program::parse(initial_source).unwrap().0.unwrap();
13684
13685 let mut frontend = FrontendState::new();
13686 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13687 let version = Version(0);
13688
13689 frontend.hack_set_program(&ctx, program).await.unwrap();
13690 let wall_object_id = find_first_wall_object_id(&frontend.scene_graph).expect("expected a wall object");
13691
13692 let sketch_args = SketchCtor {
13693 on: Plane::Object(wall_object_id),
13694 };
13695 let (src_delta, _scene_delta, _sketch_id) = frontend
13696 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
13697 .await
13698 .unwrap();
13699 assert!(src_delta.text.contains("faceOf(extrude001, face = region001.tags."));
13700
13701 ctx.close().await;
13702 }
13703
13704 #[tokio::test(flavor = "multi_thread")]
13705 async fn test_sketch_on_wall_artifact_from_split_region_extrude() {
13706 let initial_source = "\
13707sketch001 = sketch(on = YZ) {
13708 line1 = line(start = [var 0.49, var -0.39], end = [var 6.52, var -0.39])
13709 line2 = line(start = [var 6.52, var -0.39], end = [var 6.52, var 4.9])
13710 line3 = line(start = [var 6.52, var 4.9], end = [var 0.49, var 4.9])
13711 line4 = line(start = [var 0.49, var 4.9], end = [var 0.49, var -0.39])
13712 coincident([line1.end, line2.start])
13713 coincident([line2.end, line3.start])
13714 coincident([line3.end, line4.start])
13715 coincident([line4.end, line1.start])
13716 parallel([line2, line4])
13717 parallel([line3, line1])
13718 perpendicular([line1, line2])
13719 horizontal(line3)
13720 line5 = line(start = [2.35, 6.65], end = [5.89, -2.7])
13721}
13722region001 = region(point = [3.1, 3.74], sketch = sketch001)
13723extrude001 = extrude(region001, length = 5)
13724";
13725
13726 let program = Program::parse(initial_source).unwrap().0.unwrap();
13727
13728 let mut frontend = FrontendState::new();
13729 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13730 let version = Version(0);
13731
13732 frontend.hack_set_program(&ctx, program).await.unwrap();
13733 let wall_object_id = find_first_wall_object_id(&frontend.scene_graph).expect("expected a wall object");
13734
13735 let sketch_args = SketchCtor {
13736 on: Plane::Object(wall_object_id),
13737 };
13738 let (src_delta, _scene_delta, _sketch_id) = frontend
13739 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
13740 .await
13741 .unwrap();
13742 assert!(src_delta.text.contains("faceOf(extrude001, face = region001.tags."));
13743
13744 ctx.close().await;
13745 }
13746
13747 #[tokio::test(flavor = "multi_thread")]
13748 async fn test_new_sketch_on_multi_region_extrude_cap_indexes_selected_solid() {
13749 let initial_source = "\
13750@settings(kclVersion = 2.0)
13751
13752sketch001 = sketch(on = XY) {
13753 circle1 = circle(start = [var -1.51mm, var 1.31mm], center = [var -1.98mm, var 1.03mm])
13754 circle2 = circle(start = [var 2.98mm, var 2.37mm], center = [var 2.66mm, var 1.46mm])
13755}
13756hidden001 = hide(sketch001)
13757region001 = region(segments = [sketch001.circle2])
13758region002 = region(segments = [sketch001.circle1])
13759extrude001 = extrude([region001, region002], length = 5)
13760";
13761
13762 let program = Program::parse(initial_source).unwrap().0.unwrap();
13763 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13764 let version = Version(0);
13765
13766 for (solid_output_index, expected_face) in [
13767 (0, "faceOf(extrude001[0], face = END)"),
13768 (1, "faceOf(extrude001[1], face = END)"),
13769 ] {
13770 let mut frontend = FrontendState::new();
13771 frontend.hack_set_program(&ctx, program.clone()).await.unwrap();
13772 let cap_object_id = find_cap_object_id_with_solid_output_index(
13773 &frontend.scene_graph,
13774 crate::frontend::api::CapKind::End,
13775 solid_output_index,
13776 )
13777 .unwrap_or_else(|| panic!("expected an end cap object for solid output index {solid_output_index}"));
13778
13779 let sketch_args = SketchCtor {
13780 on: Plane::Object(cap_object_id),
13781 };
13782 let (src_delta, _scene_delta, _sketch_id) = frontend
13783 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
13784 .await
13785 .unwrap();
13786
13787 assert!(
13788 src_delta.text.contains(expected_face),
13789 "expected `{expected_face}` in:\n{}",
13790 src_delta.text
13791 );
13792 assert!(!src_delta.text.contains("faceOf(extrude001, face = END)"));
13793 }
13794
13795 ctx.close().await;
13796 }
13797
13798 #[tokio::test(flavor = "multi_thread")]
13799 async fn test_new_sketch_on_multi_region_extrude_wall_indexes_selected_solid() {
13800 let initial_source = "\
13801@settings(kclVersion = 2.0)
13802
13803sketch001 = sketch(on = XY) {
13804 rect1Line1 = line(start = [0, 0], end = [1, 0])
13805 rect1Line2 = line(start = [1, 0], end = [1, 1])
13806 rect1Line3 = line(start = [1, 1], end = [0, 1])
13807 rect1Line4 = line(start = [0, 1], end = [0, 0])
13808 rect2Line1 = line(start = [3, 0], end = [4, 0])
13809 rect2Line2 = line(start = [4, 0], end = [4, 1])
13810 rect2Line3 = line(start = [4, 1], end = [3, 1])
13811 rect2Line4 = line(start = [3, 1], end = [3, 0])
13812}
13813hidden001 = hide(sketch001)
13814region001 = region(segments = [
13815 sketch001.rect1Line4,
13816 sketch001.rect1Line1
13817])
13818region002 = region(segments = [
13819 sketch001.rect2Line4,
13820 sketch001.rect2Line1
13821])
13822extrude001 = extrude([region001, region002], length = 5)
13823";
13824
13825 let program = Program::parse(initial_source).unwrap().0.unwrap();
13826 let mut frontend = FrontendState::new();
13827 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13828 let version = Version(0);
13829
13830 frontend.hack_set_program(&ctx, program).await.unwrap();
13831 let region_call = "\
13832region(segments = [
13833 sketch001.rect1Line4,
13834 sketch001.rect1Line1
13835])";
13836 let region_call_start = initial_source.find(region_call).unwrap();
13837 let region_range = [region_call_start, region_call_start + region_call.len(), 0].into();
13838 let segment_call = "line(start = [0, 0], end = [1, 0])";
13839 let segment_call_start = initial_source.find(segment_call).unwrap();
13840 let segment_range = [segment_call_start, segment_call_start + segment_call.len(), 0].into();
13841 let wall_object_id = frontend
13842 .scene_graph
13843 .objects
13844 .iter()
13845 .find_map(|object| match &object.kind {
13846 ObjectKind::Wall(wall)
13847 if wall.source.path.as_ref().is_some_and(|path| path.range == region_range)
13848 && wall.source.segment.range == segment_range =>
13849 {
13850 Some(object.id)
13851 }
13852 _ => None,
13853 })
13854 .expect("expected a wall object for region001.tags.rect1Line1");
13855
13856 let sketch_args = SketchCtor {
13857 on: Plane::Object(wall_object_id),
13858 };
13859 let (src_delta, _scene_delta, _sketch_id) = frontend
13860 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
13861 .await
13862 .unwrap();
13863
13864 let expected_face = "faceOf(extrude001[0], face = region001.tags.rect1Line1)";
13865 assert!(
13866 src_delta.text.contains(expected_face),
13867 "expected `{expected_face}` in:\n{}",
13868 src_delta.text
13869 );
13870 assert!(!src_delta.text.contains("faceOf(extrude001, face ="));
13871
13872 ctx.close().await;
13873 }
13874
13875 #[test]
13876 fn test_enclosing_variable_fallback_skips_nested_sketch_items() {
13877 let source = "\
13878sketch001 = sketch(on = XY) {
13879 line(start = [0, 0], end = [1, 0])
13880}
13881part = subtract(boxSolid, tools = [cutSolid])
13882 |> appearance(color = \"#8f96a3\")
13883";
13884 let ast = Program::parse(source).unwrap().0.unwrap().ast;
13885 let line_start = source.find("line").unwrap();
13886 let line_end = line_start + "line(start = [0, 0], end = [1, 0])".len();
13887 let line_ref = SourceRef::Simple {
13888 range: [line_start, line_end, 0].into(),
13889 node_path: None,
13890 };
13891 assert_eq!(variable_name_containing_source_ref(&ast, &line_ref), None);
13892
13893 let subtract_start = source.find("subtract").unwrap();
13894 let subtract_end = subtract_start + "subtract(boxSolid, tools = [cutSolid])".len();
13895 let subtract_ref = SourceRef::Simple {
13896 range: [subtract_start, subtract_end, 0].into(),
13897 node_path: None,
13898 };
13899 assert_eq!(
13900 variable_name_containing_source_ref(&ast, &subtract_ref),
13901 Some("part".to_owned())
13902 );
13903 }
13904
13905 #[tokio::test(flavor = "multi_thread")]
13906 async fn test_sketch_on_subtracted_sweep_cap_uses_composite_solid() {
13907 clear_mem_cache().await;
13908 let source = "\
13909boxSolid = startSketchOn(XY)
13910 |> startProfile(at = [0, 0])
13911 |> line(end = [4, 0], tag = $bottomEdge)
13912 |> line(end = [0, 4])
13913 |> line(end = [-4, 0])
13914 |> close()
13915 |> extrude(length = 10)
13916cutSolid = startSketchOn(XY)
13917 |> startProfile(at = [1, 1])
13918 |> line(end = [1, 0])
13919 |> line(end = [0, 1])
13920 |> line(end = [-1, 0])
13921 |> close()
13922 |> extrude(length = 10)
13923part = subtract(boxSolid, tools = [cutSolid])
13924 |> appearance(color = \"#8f96a3\", roughness = 55, metalness = 8)
13925";
13926 let program = Program::parse(source).unwrap().0.unwrap();
13927 let mut frontend = FrontendState::new();
13928 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13929 match frontend.hack_set_program(&ctx, program).await.unwrap() {
13930 SetProgramOutcome::Success { .. } => {}
13931 SetProgramOutcome::ExecFailure { error } => panic!("KCL fixture failed to execute: {error:?}"),
13932 }
13933
13934 let sweep_call_start = source.find("extrude").unwrap();
13935 let sweep_call_end = sweep_call_start + "extrude(length = 10)".len();
13936 let part_call_start = source.find("subtract").unwrap();
13937 let part_call_end = part_call_start + "subtract(boxSolid, tools = [cutSolid])".len();
13938 let sweep_range = [sweep_call_start, sweep_call_end, 0].into();
13939 let composite_range = [part_call_start, part_call_end, 0].into();
13940
13941 let cap_object = frontend
13942 .scene_graph
13943 .objects
13944 .iter()
13945 .find(|object| {
13946 matches!(
13947 &object.kind,
13948 ObjectKind::Cap(crate::frontend::api::Cap {
13949 kind: crate::frontend::api::CapKind::End,
13950 source,
13951 ..
13952 }) if source.solid.range == composite_range && source.sweep.range == sweep_range
13953 )
13954 })
13955 .expect("expected end cap object to trace through subtract and original extrude");
13956
13957 let mut ast = frontend.program.ast.clone();
13958 let cap_expr = sketch_on_ast_expr(
13959 &mut ast,
13960 &frontend.scene_graph,
13961 &frontend.solid_references,
13962 &Plane::Object(cap_object.id),
13963 )
13964 .unwrap();
13965 let cap_face_decl = ast::VariableDeclaration::new(
13966 ast::VariableDeclarator::new("capFace", cap_expr.clone()),
13967 ast::ItemVisibility::Default,
13968 ast::VariableKind::Const,
13969 );
13970 ast.body
13971 .push(ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(
13972 cap_face_decl,
13973 ))));
13974 let generated_source = source_from_ast(&ast);
13975
13976 assert!(generated_source.contains("capFace = faceOf(part, face = END)"));
13977 assert!(!generated_source.contains("faceOf(boxSolid"));
13978 let ast::Expr::CallExpressionKw(call) = cap_expr else {
13979 panic!("expected faceOf call");
13980 };
13981 assert_eq!(call.callee.name.name, "faceOf");
13982 let ast::Expr::Name(solid_name) = call.unlabeled.as_ref().unwrap() else {
13983 panic!("expected solid name");
13984 };
13985 assert_eq!(solid_name.name.name, "part");
13986 let ast::Expr::Name(face_name) = &call.arguments[0].arg else {
13987 panic!("expected face name");
13988 };
13989 assert_eq!(face_name.name.name, "END");
13990
13991 ctx.close().await;
13992 }
13993
13994 #[tokio::test(flavor = "multi_thread")]
13995 async fn test_sketch_on_plane_incremental() {
13996 let initial_source = "\
13997len = 2mm
13998cube = startSketchOn(XY)
13999 |> startProfile(at = [0, 0])
14000 |> line(end = [len, 0], tag = $side)
14001 |> line(end = [0, len])
14002 |> line(end = [-len, 0])
14003 |> line(end = [0, -len])
14004 |> close()
14005 |> extrude(length = len)
14006
14007plane = planeOf(cube, face = side)
14008";
14009
14010 let program = Program::parse(initial_source).unwrap().0.unwrap();
14011
14012 let mut frontend = FrontendState::new();
14013
14014 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14015 let mock_ctx = ExecutorContext::new_mock(None).await;
14016 let version = Version(0);
14017
14018 frontend.hack_set_program(&ctx, program).await.unwrap();
14019 let plane_object = frontend
14021 .scene_graph
14022 .objects
14023 .iter()
14024 .rev()
14025 .find(|object| matches!(&object.kind, ObjectKind::Plane(_)))
14026 .unwrap();
14027 let plane_id = plane_object.id;
14028
14029 let sketch_args = SketchCtor {
14030 on: Plane::Object(plane_id),
14031 };
14032 let (src_delta, scene_delta, sketch_id) = frontend
14033 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14034 .await
14035 .unwrap();
14036 insta::assert_snapshot!("test_sketch_on_plane_incremental", src_delta.text.as_str());
14037 assert_eq!(sketch_id, ObjectId(2));
14038 assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
14039 let sketch_object = &scene_delta.new_graph.objects[2];
14040 assert_eq!(sketch_object.id, ObjectId(2));
14041 assert_eq!(
14042 sketch_object.kind,
14043 ObjectKind::Sketch(Sketch {
14044 args: SketchCtor {
14045 on: Plane::Object(plane_id),
14046 },
14047 plane: plane_id,
14048 segments: vec![],
14049 constraints: vec![],
14050 })
14051 );
14052 assert_eq!(scene_delta.new_graph.objects.len(), 9);
14053
14054 let plane_object = scene_delta.new_graph.objects.get(plane_id.0).unwrap();
14055 assert_eq!(plane_object.id, plane_id);
14056 assert_eq!(plane_object.kind, ObjectKind::Plane(Plane::Object(plane_id)));
14057
14058 ctx.close().await;
14059 mock_ctx.close().await;
14060 }
14061
14062 #[tokio::test(flavor = "multi_thread")]
14063 async fn test_new_sketch_uses_unique_variable_name() {
14064 let initial_source = "\
14065sketch1 = sketch(on = XY) {
14066}
14067";
14068
14069 let program = Program::parse(initial_source).unwrap().0.unwrap();
14070
14071 let mut frontend = FrontendState::new();
14072 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14073 let version = Version(0);
14074
14075 frontend.hack_set_program(&ctx, program).await.unwrap();
14076
14077 let sketch_args = SketchCtor {
14078 on: Plane::Default(PlaneName::Yz),
14079 };
14080 let (src_delta, _, _) = frontend
14081 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14082 .await
14083 .unwrap();
14084
14085 insta::assert_snapshot!("test_new_sketch_uses_unique_variable_name", src_delta.text.as_str());
14086
14087 ctx.close().await;
14088 }
14089
14090 #[tokio::test(flavor = "multi_thread")]
14091 async fn test_new_sketch_twice_using_same_plane() {
14092 let initial_source = "\
14093sketch1 = sketch(on = XY) {
14094}
14095";
14096
14097 let program = Program::parse(initial_source).unwrap().0.unwrap();
14098
14099 let mut frontend = FrontendState::new();
14100 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14101 let version = Version(0);
14102
14103 frontend.hack_set_program(&ctx, program).await.unwrap();
14104
14105 let sketch_args = SketchCtor {
14106 on: Plane::Default(PlaneName::Xy),
14107 };
14108 let (src_delta, _, _) = frontend
14109 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14110 .await
14111 .unwrap();
14112
14113 insta::assert_snapshot!("test_new_sketch_twice_using_same_plane", src_delta.text.as_str());
14114
14115 ctx.close().await;
14116 }
14117
14118 #[tokio::test(flavor = "multi_thread")]
14119 async fn test_sketch_mode_reuses_cached_on_expression() {
14120 let initial_source = "\
14121width = 2mm
14122sketch(on = offsetPlane(XY, offset = width)) {
14123 line1 = line(start = [var 0, var 0], end = [var 1mm, var 0])
14124 distance([line1.start, line1.end]) == width
14125}
14126";
14127 let program = Program::parse(initial_source).unwrap().0.unwrap();
14128
14129 let mut frontend = FrontendState::new();
14130 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14131 let mock_ctx = ExecutorContext::new_mock(None).await;
14132 let version = Version(0);
14133 let project_id = ProjectId(0);
14134 let file_id = FileId(0);
14135
14136 frontend.hack_set_program(&ctx, program).await.unwrap();
14137 let initial_object_count = frontend.scene_graph.objects.len();
14138 let sketch_id = find_first_sketch_object(&frontend.scene_graph)
14139 .expect("Expected sketch object to exist")
14140 .id;
14141
14142 let scene_delta = frontend
14145 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
14146 .await
14147 .unwrap();
14148 assert_eq!(scene_delta.new_graph.objects.len(), initial_object_count);
14149
14150 let (_src_delta, scene_delta) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
14153 assert_eq!(scene_delta.new_graph.objects.len(), initial_object_count);
14154
14155 ctx.close().await;
14156 mock_ctx.close().await;
14157 }
14158
14159 #[tokio::test(flavor = "multi_thread")]
14160 async fn test_multiple_sketch_blocks() {
14161 let initial_source = "\
14162// Cube that requires the engine.
14163width = 2
14164sketch001 = startSketchOn(XY)
14165profile001 = startProfile(sketch001, at = [0, 0])
14166 |> yLine(length = width, tag = $seg1)
14167 |> xLine(length = width)
14168 |> yLine(length = -width)
14169 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
14170 |> close()
14171extrude001 = extrude(profile001, length = width)
14172
14173// Get a value that requires the engine.
14174x = segLen(seg1)
14175
14176// Triangle with side length 2*x.
14177sketch(on = XY) {
14178 line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
14179 line2 = line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
14180 coincident([line1.end, line2.start])
14181 line3 = line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
14182 coincident([line2.end, line3.start])
14183 coincident([line3.end, line1.start])
14184 equalLength([line3, line1])
14185 equalLength([line1, line2])
14186 distance([line1.start, line1.end]) == 2*x
14187}
14188
14189// Line segment with length x.
14190sketch2 = sketch(on = XY) {
14191 line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
14192 distance([line1.start, line1.end]) == x
14193}
14194";
14195
14196 let program = Program::parse(initial_source).unwrap().0.unwrap();
14197
14198 let mut frontend = FrontendState::new();
14199
14200 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14201 let mock_ctx = ExecutorContext::new_mock(None).await;
14202 let version = Version(0);
14203 let project_id = ProjectId(0);
14204 let file_id = FileId(0);
14205
14206 frontend.hack_set_program(&ctx, program).await.unwrap();
14207 let sketch_objects = frontend
14208 .scene_graph
14209 .objects
14210 .iter()
14211 .filter(|obj| matches!(obj.kind, ObjectKind::Sketch(_)))
14212 .collect::<Vec<_>>();
14213 let sketch1_id = sketch_objects.first().unwrap().id;
14214 let sketch2_id = sketch_objects.get(1).unwrap().id;
14215 let point1_id = ObjectId(sketch1_id.0 + 1);
14217 let point2_id = ObjectId(sketch2_id.0 + 1);
14219
14220 let scene_delta = frontend
14229 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch1_id)
14230 .await
14231 .unwrap();
14232 assert_eq!(
14233 scene_delta.new_graph.objects.len(),
14234 18,
14235 "{:#?}",
14236 scene_delta.new_graph.objects
14237 );
14238
14239 let point_ctor = PointCtor {
14241 position: Point2d {
14242 x: Expr::Var(Number {
14243 value: 1.0,
14244 units: NumericSuffix::Mm,
14245 }),
14246 y: Expr::Var(Number {
14247 value: 2.0,
14248 units: NumericSuffix::Mm,
14249 }),
14250 },
14251 };
14252 let segments = vec![ExistingSegmentCtor {
14253 id: point1_id,
14254 ctor: SegmentCtor::Point(point_ctor),
14255 }];
14256 let (src_delta, _) = frontend
14257 .edit_segments(&mock_ctx, version, sketch1_id, segments)
14258 .await
14259 .unwrap();
14260 insta::assert_snapshot!("test_multiple_sketch_blocks_1", src_delta.text.as_str());
14262 let edited_sketch1_source = src_delta.text.clone();
14263
14264 let (src_delta, _) = frontend.execute_mock(&mock_ctx, version, sketch1_id).await.unwrap();
14266 assert_eq!(src_delta.text, edited_sketch1_source);
14267 let scene = frontend.exit_sketch(&ctx, version, sketch1_id).await.unwrap();
14275 assert_eq!(scene.objects.len(), 30, "{:#?}", scene.objects);
14276
14277 let scene_delta = frontend
14285 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch2_id)
14286 .await
14287 .unwrap();
14288 assert_eq!(
14289 scene_delta.new_graph.objects.len(),
14290 24,
14291 "{:#?}",
14292 scene_delta.new_graph.objects
14293 );
14294
14295 let point_ctor = PointCtor {
14297 position: Point2d {
14298 x: Expr::Var(Number {
14299 value: 3.0,
14300 units: NumericSuffix::Mm,
14301 }),
14302 y: Expr::Var(Number {
14303 value: 4.0,
14304 units: NumericSuffix::Mm,
14305 }),
14306 },
14307 };
14308 let segments = vec![ExistingSegmentCtor {
14309 id: point2_id,
14310 ctor: SegmentCtor::Point(point_ctor),
14311 }];
14312 let (src_delta, _) = frontend
14313 .edit_segments(&mock_ctx, version, sketch2_id, segments)
14314 .await
14315 .unwrap();
14316 insta::assert_snapshot!("test_multiple_sketch_blocks_2", src_delta.text.as_str());
14318 let edited_sketch2_source = src_delta.text.clone();
14319
14320 let (src_delta, _) = frontend.execute_mock(&mock_ctx, version, sketch2_id).await.unwrap();
14322 assert_eq!(src_delta.text, edited_sketch2_source);
14323
14324 ctx.close().await;
14325 mock_ctx.close().await;
14326 }
14327
14328 #[tokio::test(flavor = "multi_thread")]
14329 async fn test_exit_sketch_without_changes_allows_entering_next_sketch() {
14330 clear_mem_cache().await;
14331
14332 let source = r#"sketch001 = sketch(on = XZ) {
14333 circle1 = circle(start = [var -1.96mm, var 2.77mm], center = [var -2.69mm, var 3.44mm])
14334}
14335sketch002 = sketch(on = XY) {
14336 line1 = line(start = [var 0mm, var 0mm], end = [var 4.68mm, var 0mm])
14337 line2 = line(start = [var 4.68mm, var 0mm], end = [var 4.68mm, var 2.96mm])
14338 line3 = line(start = [var 4.68mm, var 2.96mm], end = [var 0mm, var 2.96mm])
14339 line4 = line(start = [var 0mm, var 2.96mm], end = [var 0mm, var 0mm])
14340 coincident([line1.end, line2.start])
14341 coincident([line2.end, line3.start])
14342 coincident([line3.end, line4.start])
14343 coincident([line4.end, line1.start])
14344 parallel([line2, line4])
14345 parallel([line3, line1])
14346 perpendicular([line1, line2])
14347 horizontal(line3)
14348 coincident([line1.start, ORIGIN])
14349}
14350"#;
14351
14352 let program = Program::parse(source).unwrap().0.unwrap();
14353 let mut frontend = FrontendState::new();
14354 let ctx = ExecutorContext::new_with_engine(sync::Arc::new(EngineManager::new_mock()), Default::default());
14355 let mock_ctx = ExecutorContext::new_mock(None).await;
14356 let version = Version(0);
14357 let project_id = ProjectId(0);
14358 let file_id = FileId(0);
14359
14360 frontend.hack_set_program(&ctx, program).await.unwrap();
14361 let sketch_objects = frontend
14362 .scene_graph
14363 .objects
14364 .iter()
14365 .filter(|object| matches!(object.kind, ObjectKind::Sketch(_)))
14366 .collect::<Vec<_>>();
14367 assert_eq!(sketch_objects.len(), 2, "{:#?}", frontend.scene_graph.objects);
14368
14369 let sketch1_id = sketch_objects[0].id;
14370 let sketch2_id = sketch_objects[1].id;
14371
14372 frontend
14373 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch1_id)
14374 .await
14375 .unwrap();
14376 frontend.exit_sketch(&ctx, version, sketch1_id).await.unwrap();
14377
14378 let scene_delta = frontend
14379 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch2_id)
14380 .await
14381 .unwrap();
14382 assert_eq!(scene_delta.new_graph.sketch_mode, Some(sketch2_id));
14383
14384 clear_mem_cache().await;
14385 ctx.close().await;
14386 mock_ctx.close().await;
14387 }
14388
14389 #[tokio::test(flavor = "multi_thread")]
14394 async fn test_extra_newlines_after_settings_edit_sketch_add_point() {
14395 let initial_source = "@settings(defaultLengthUnit = mm)
14397
14398sketch001 = sketch(on = XY) {
14399 point(at = [1in, 2in])
14400}
14401";
14402
14403 let program = Program::parse(initial_source).unwrap().0.unwrap();
14404 let mut frontend = FrontendState::new();
14405
14406 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14407 let mock_ctx = ExecutorContext::new_mock(None).await;
14408 let version = Version(0);
14409 let project_id = ProjectId(0);
14410 let file_id = FileId(0);
14411
14412 frontend.hack_set_program(&ctx, program).await.unwrap();
14413 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14414 let sketch_id = sketch_object.id;
14415
14416 frontend
14418 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
14419 .await
14420 .unwrap();
14421
14422 let point_ctor = PointCtor {
14424 position: Point2d {
14425 x: Expr::Number(Number {
14426 value: 5.0,
14427 units: NumericSuffix::Mm,
14428 }),
14429 y: Expr::Number(Number {
14430 value: 6.0,
14431 units: NumericSuffix::Mm,
14432 }),
14433 },
14434 };
14435 let segment = SegmentCtor::Point(point_ctor);
14436 let (src_delta, scene_delta) = frontend
14437 .add_segment(&mock_ctx, version, sketch_id, segment, None)
14438 .await
14439 .unwrap();
14440 assert!(
14442 src_delta.text.contains("point(at = [5mm, 6mm])"),
14443 "Expected new point in source, got: {}",
14444 src_delta.text
14445 );
14446 assert!(!scene_delta.new_objects.is_empty());
14447
14448 ctx.close().await;
14449 mock_ctx.close().await;
14450 }
14451
14452 #[tokio::test(flavor = "multi_thread")]
14453 async fn test_ensure_control_point_spline_experimental_features_adds_allow_setting() {
14454 let initial_program = Program::parse("s = sketch(on = XY) {}\n").unwrap().0.unwrap();
14455
14456 let updated_program = ensure_control_point_spline_experimental_features(&initial_program).unwrap();
14457 let meta_settings = updated_program.meta_settings().unwrap().unwrap();
14458
14459 assert_eq!(meta_settings.experimental_features, WarningLevel::Allow);
14460 assert!(
14461 source_from_ast(&updated_program.ast).contains("@settings(experimentalFeatures = allow)"),
14462 "Expected experimental settings to be added to source"
14463 );
14464 }
14465
14466 #[tokio::test(flavor = "multi_thread")]
14467 async fn test_extra_newlines_after_settings_add_line_to_empty_sketch() {
14468 let initial_source = "@settings(defaultLengthUnit = mm)
14470
14471s = sketch(on = XY) {}
14472";
14473
14474 let program = Program::parse(initial_source).unwrap().0.unwrap();
14475 let mut frontend = FrontendState::new();
14476
14477 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14478 let mock_ctx = ExecutorContext::new_mock(None).await;
14479 let version = Version(0);
14480
14481 frontend.hack_set_program(&ctx, program).await.unwrap();
14482 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14483 let sketch_id = sketch_object.id;
14484
14485 let line_ctor = LineCtor {
14486 start: Point2d {
14487 x: Expr::Number(Number {
14488 value: 0.0,
14489 units: NumericSuffix::Mm,
14490 }),
14491 y: Expr::Number(Number {
14492 value: 0.0,
14493 units: NumericSuffix::Mm,
14494 }),
14495 },
14496 end: Point2d {
14497 x: Expr::Number(Number {
14498 value: 10.0,
14499 units: NumericSuffix::Mm,
14500 }),
14501 y: Expr::Number(Number {
14502 value: 10.0,
14503 units: NumericSuffix::Mm,
14504 }),
14505 },
14506 construction: None,
14507 };
14508 let segment = SegmentCtor::Line(line_ctor);
14509 let (src_delta, scene_delta) = frontend
14510 .add_segment(&mock_ctx, version, sketch_id, segment, None)
14511 .await
14512 .unwrap();
14513 assert!(
14514 src_delta.text.contains("line(start = [0mm, 0mm], end = [10mm, 10mm])"),
14515 "Expected line in source, got: {}",
14516 src_delta.text
14517 );
14518 assert_eq!(scene_delta.new_objects.len(), 3);
14520
14521 ctx.close().await;
14522 mock_ctx.close().await;
14523 }
14524
14525 #[tokio::test(flavor = "multi_thread")]
14526 async fn test_extra_newlines_between_operations_edit_line() {
14527 let initial_source = "@settings(defaultLengthUnit = mm)
14529
14530sketch001 = sketch(on = XY) {
14531
14532 line1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm])
14533
14534}
14535";
14536
14537 let program = Program::parse(initial_source).unwrap().0.unwrap();
14538 let mut frontend = FrontendState::new();
14539
14540 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14541 let mock_ctx = ExecutorContext::new_mock(None).await;
14542 let version = Version(0);
14543 let project_id = ProjectId(0);
14544 let file_id = FileId(0);
14545
14546 frontend.hack_set_program(&ctx, program).await.unwrap();
14547 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14548 let sketch_id = sketch_object.id;
14549 let sketch = expect_sketch(sketch_object);
14550
14551 let line_id = sketch
14553 .segments
14554 .iter()
14555 .copied()
14556 .find(|seg_id| {
14557 matches!(
14558 &frontend.scene_graph.objects[seg_id.0].kind,
14559 ObjectKind::Segment {
14560 segment: Segment::Line(_)
14561 }
14562 )
14563 })
14564 .expect("Expected a line segment in sketch");
14565
14566 frontend
14568 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
14569 .await
14570 .unwrap();
14571
14572 let line_ctor = LineCtor {
14574 start: Point2d {
14575 x: Expr::Var(Number {
14576 value: 1.0,
14577 units: NumericSuffix::Mm,
14578 }),
14579 y: Expr::Var(Number {
14580 value: 2.0,
14581 units: NumericSuffix::Mm,
14582 }),
14583 },
14584 end: Point2d {
14585 x: Expr::Var(Number {
14586 value: 13.0,
14587 units: NumericSuffix::Mm,
14588 }),
14589 y: Expr::Var(Number {
14590 value: 14.0,
14591 units: NumericSuffix::Mm,
14592 }),
14593 },
14594 construction: None,
14595 };
14596 let segments = vec![ExistingSegmentCtor {
14597 id: line_id,
14598 ctor: SegmentCtor::Line(line_ctor),
14599 }];
14600 let (src_delta, _scene_delta) = frontend
14601 .edit_segments(&mock_ctx, version, sketch_id, segments)
14602 .await
14603 .unwrap();
14604 assert!(
14605 src_delta
14606 .text
14607 .contains("line(start = [var 1mm, var 2mm], end = [var 13mm, var 14mm])"),
14608 "Expected edited line in source, got: {}",
14609 src_delta.text
14610 );
14611
14612 ctx.close().await;
14613 mock_ctx.close().await;
14614 }
14615
14616 #[tokio::test(flavor = "multi_thread")]
14617 async fn test_extra_newlines_delete_segment() {
14618 let initial_source = "@settings(defaultLengthUnit = mm)
14620
14621sketch001 = sketch(on = XY) {
14622 circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
14623}
14624";
14625
14626 let program = Program::parse(initial_source).unwrap().0.unwrap();
14627 let mut frontend = FrontendState::new();
14628
14629 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14630 let mock_ctx = ExecutorContext::new_mock(None).await;
14631 let version = Version(0);
14632
14633 frontend.hack_set_program(&ctx, program).await.unwrap();
14634 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14635 let sketch_id = sketch_object.id;
14636 let sketch = expect_sketch(sketch_object);
14637
14638 assert_eq!(sketch.segments.len(), 3);
14640 let circle_id = sketch.segments[2];
14641
14642 let (src_delta, scene_delta) = frontend
14644 .delete_objects(&mock_ctx, version, sketch_id, vec![], vec![circle_id])
14645 .await
14646 .unwrap();
14647 assert!(
14648 src_delta.text.contains("sketch(on = XY) {"),
14649 "Expected sketch block in source, got: {}",
14650 src_delta.text
14651 );
14652 let new_sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
14653 let new_sketch = expect_sketch(new_sketch_object);
14654 assert_eq!(new_sketch.segments.len(), 0);
14655
14656 ctx.close().await;
14657 mock_ctx.close().await;
14658 }
14659
14660 #[tokio::test(flavor = "multi_thread")]
14661 async fn test_unformatted_source_add_arc() {
14662 let initial_source = "@settings(defaultLengthUnit = mm)
14664
14665sketch001 = sketch(on = XY) {
14666}
14667";
14668
14669 let program = Program::parse(initial_source).unwrap().0.unwrap();
14670 let mut frontend = FrontendState::new();
14671
14672 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14673 let mock_ctx = ExecutorContext::new_mock(None).await;
14674 let version = Version(0);
14675
14676 frontend.hack_set_program(&ctx, program).await.unwrap();
14677 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14678 let sketch_id = sketch_object.id;
14679
14680 let arc_ctor = ArcCtor {
14681 start: Point2d {
14682 x: Expr::Var(Number {
14683 value: 5.0,
14684 units: NumericSuffix::Mm,
14685 }),
14686 y: Expr::Var(Number {
14687 value: 0.0,
14688 units: NumericSuffix::Mm,
14689 }),
14690 },
14691 end: Point2d {
14692 x: Expr::Var(Number {
14693 value: 0.0,
14694 units: NumericSuffix::Mm,
14695 }),
14696 y: Expr::Var(Number {
14697 value: 5.0,
14698 units: NumericSuffix::Mm,
14699 }),
14700 },
14701 center: Point2d {
14702 x: Expr::Var(Number {
14703 value: 0.0,
14704 units: NumericSuffix::Mm,
14705 }),
14706 y: Expr::Var(Number {
14707 value: 0.0,
14708 units: NumericSuffix::Mm,
14709 }),
14710 },
14711 construction: None,
14712 };
14713 let segment = SegmentCtor::Arc(arc_ctor);
14714 let (src_delta, scene_delta) = frontend
14715 .add_segment(&mock_ctx, version, sketch_id, segment, None)
14716 .await
14717 .unwrap();
14718 assert!(
14719 src_delta
14720 .text
14721 .contains("arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])"),
14722 "Expected arc in source, got: {}",
14723 src_delta.text
14724 );
14725 assert!(!scene_delta.new_objects.is_empty());
14726
14727 ctx.close().await;
14728 mock_ctx.close().await;
14729 }
14730
14731 #[tokio::test(flavor = "multi_thread")]
14732 async fn test_extra_newlines_add_circle() {
14733 let initial_source = "@settings(defaultLengthUnit = mm)
14735
14736sketch001 = sketch(on = XY) {
14737}
14738";
14739
14740 let program = Program::parse(initial_source).unwrap().0.unwrap();
14741 let mut frontend = FrontendState::new();
14742
14743 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14744 let mock_ctx = ExecutorContext::new_mock(None).await;
14745 let version = Version(0);
14746
14747 frontend.hack_set_program(&ctx, program).await.unwrap();
14748 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14749 let sketch_id = sketch_object.id;
14750
14751 let circle_ctor = CircleCtor {
14752 start: Point2d {
14753 x: Expr::Var(Number {
14754 value: 5.0,
14755 units: NumericSuffix::Mm,
14756 }),
14757 y: Expr::Var(Number {
14758 value: 0.0,
14759 units: NumericSuffix::Mm,
14760 }),
14761 },
14762 center: Point2d {
14763 x: Expr::Var(Number {
14764 value: 0.0,
14765 units: NumericSuffix::Mm,
14766 }),
14767 y: Expr::Var(Number {
14768 value: 0.0,
14769 units: NumericSuffix::Mm,
14770 }),
14771 },
14772 construction: None,
14773 };
14774 let segment = SegmentCtor::Circle(circle_ctor);
14775 let (src_delta, scene_delta) = frontend
14776 .add_segment(&mock_ctx, version, sketch_id, segment, None)
14777 .await
14778 .unwrap();
14779 assert!(
14780 src_delta
14781 .text
14782 .contains("circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])"),
14783 "Expected circle in source, got: {}",
14784 src_delta.text
14785 );
14786 assert!(!scene_delta.new_objects.is_empty());
14787
14788 ctx.close().await;
14789 mock_ctx.close().await;
14790 }
14791
14792 #[tokio::test(flavor = "multi_thread")]
14793 async fn test_extra_newlines_add_constraint() {
14794 let initial_source = "@settings(defaultLengthUnit = mm)
14796
14797sketch001 = sketch(on = XY) {
14798 line1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm])
14799 line2 = line(start = [var 10mm, var 10mm], end = [var 20mm, var 0mm])
14800}
14801";
14802
14803 let program = Program::parse(initial_source).unwrap().0.unwrap();
14804 let mut frontend = FrontendState::new();
14805
14806 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14807 let mock_ctx = ExecutorContext::new_mock(None).await;
14808 let version = Version(0);
14809 let project_id = ProjectId(0);
14810 let file_id = FileId(0);
14811
14812 frontend.hack_set_program(&ctx, program).await.unwrap();
14813 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14814 let sketch_id = sketch_object.id;
14815 let sketch = expect_sketch(sketch_object);
14816
14817 let line_ids: Vec<ObjectId> = sketch
14819 .segments
14820 .iter()
14821 .copied()
14822 .filter(|seg_id| {
14823 matches!(
14824 &frontend.scene_graph.objects[seg_id.0].kind,
14825 ObjectKind::Segment {
14826 segment: Segment::Line(_)
14827 }
14828 )
14829 })
14830 .collect();
14831 assert_eq!(line_ids.len(), 2, "Expected two line segments");
14832
14833 let line1 = &frontend.scene_graph.objects[line_ids[0].0];
14834 let ObjectKind::Segment {
14835 segment: Segment::Line(line1_data),
14836 } = &line1.kind
14837 else {
14838 panic!("Expected line");
14839 };
14840 let line2 = &frontend.scene_graph.objects[line_ids[1].0];
14841 let ObjectKind::Segment {
14842 segment: Segment::Line(line2_data),
14843 } = &line2.kind
14844 else {
14845 panic!("Expected line");
14846 };
14847
14848 let constraint = Constraint::Coincident(Coincident {
14850 segments: vec![line1_data.end.into(), line2_data.start.into()],
14851 });
14852
14853 frontend
14855 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
14856 .await
14857 .unwrap();
14858 let (src_delta, _scene_delta) = frontend
14859 .add_constraint(&mock_ctx, version, sketch_id, constraint)
14860 .await
14861 .unwrap();
14862 assert!(
14863 src_delta.text.contains("coincident("),
14864 "Expected coincident constraint in source, got: {}",
14865 src_delta.text
14866 );
14867
14868 ctx.close().await;
14869 mock_ctx.close().await;
14870 }
14871
14872 #[tokio::test(flavor = "multi_thread")]
14873 async fn test_extra_newlines_add_line_then_edit_line() {
14874 let initial_source = "@settings(defaultLengthUnit = mm)
14876
14877sketch001 = sketch(on = XY) {
14878}
14879";
14880
14881 let program = Program::parse(initial_source).unwrap().0.unwrap();
14882 let mut frontend = FrontendState::new();
14883
14884 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14885 let mock_ctx = ExecutorContext::new_mock(None).await;
14886 let version = Version(0);
14887
14888 frontend.hack_set_program(&ctx, program).await.unwrap();
14889 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14890 let sketch_id = sketch_object.id;
14891
14892 let line_ctor = LineCtor {
14894 start: Point2d {
14895 x: Expr::Number(Number {
14896 value: 0.0,
14897 units: NumericSuffix::Mm,
14898 }),
14899 y: Expr::Number(Number {
14900 value: 0.0,
14901 units: NumericSuffix::Mm,
14902 }),
14903 },
14904 end: Point2d {
14905 x: Expr::Number(Number {
14906 value: 10.0,
14907 units: NumericSuffix::Mm,
14908 }),
14909 y: Expr::Number(Number {
14910 value: 10.0,
14911 units: NumericSuffix::Mm,
14912 }),
14913 },
14914 construction: None,
14915 };
14916 let segment = SegmentCtor::Line(line_ctor);
14917 let (src_delta, scene_delta) = frontend
14918 .add_segment(&mock_ctx, version, sketch_id, segment, None)
14919 .await
14920 .unwrap();
14921 assert!(
14922 src_delta.text.contains("line(start = [0mm, 0mm], end = [10mm, 10mm])"),
14923 "Expected line in source after add, got: {}",
14924 src_delta.text
14925 );
14926 let line_id = *scene_delta.new_objects.last().unwrap();
14928
14929 let line_ctor = LineCtor {
14931 start: Point2d {
14932 x: Expr::Number(Number {
14933 value: 1.0,
14934 units: NumericSuffix::Mm,
14935 }),
14936 y: Expr::Number(Number {
14937 value: 2.0,
14938 units: NumericSuffix::Mm,
14939 }),
14940 },
14941 end: Point2d {
14942 x: Expr::Number(Number {
14943 value: 13.0,
14944 units: NumericSuffix::Mm,
14945 }),
14946 y: Expr::Number(Number {
14947 value: 14.0,
14948 units: NumericSuffix::Mm,
14949 }),
14950 },
14951 construction: None,
14952 };
14953 let segments = vec![ExistingSegmentCtor {
14954 id: line_id,
14955 ctor: SegmentCtor::Line(line_ctor),
14956 }];
14957 let (src_delta, scene_delta) = frontend
14958 .edit_segments(&mock_ctx, version, sketch_id, segments)
14959 .await
14960 .unwrap();
14961 assert!(
14962 src_delta.text.contains("line(start = [1mm, 2mm], end = [13mm, 14mm])"),
14963 "Expected edited line in source, got: {}",
14964 src_delta.text
14965 );
14966 assert_eq!(scene_delta.new_objects, vec![]);
14967
14968 ctx.close().await;
14969 mock_ctx.close().await;
14970 }
14971}