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;
12
13use crate::ExecOutcome;
14use crate::ExecutorContext;
15use crate::KclError;
16use crate::KclErrorWithOutputs;
17use crate::Program;
18use crate::SegmentDragAnchor;
19use crate::collections::AhashIndexSet;
20use crate::execution::Artifact;
21use crate::execution::ArtifactGraph;
22use crate::execution::ArtifactId;
23use crate::execution::CapSubType;
24use crate::execution::CodeRef;
25use crate::execution::MockConfig;
26use crate::execution::SKETCH_BLOCK_PARAM_ON;
27use crate::execution::annotations::WarningLevel;
28use crate::execution::cache::SketchModeState;
29use crate::execution::cache::clear_mem_cache;
30use crate::execution::cache::read_old_memory;
31use crate::execution::cache::write_old_memory;
32use crate::execution::types::adjust_length;
33use crate::fmt::format_number_literal;
34use crate::front::Angle;
35use crate::front::ArcCtor;
36use crate::front::CircleCtor;
37use crate::front::ControlPointSplineCtor;
38use crate::front::Distance;
39use crate::front::EqualRadius;
40use crate::front::Error;
41use crate::front::ExecResult;
42use crate::front::FixedPoint;
43use crate::front::Freedom;
44use crate::front::LinesEqualLength;
45use crate::front::Midpoint;
46use crate::front::Object;
47use crate::front::Parallel;
48use crate::front::Perpendicular;
49use crate::front::PointCtor;
50use crate::front::Symmetric;
51use crate::front::Tangent;
52use crate::frontend::api::CapSource;
53use crate::frontend::api::Expr;
54use crate::frontend::api::FileId;
55use crate::frontend::api::Number;
56use crate::frontend::api::ObjectId;
57use crate::frontend::api::ObjectKind;
58use crate::frontend::api::Plane;
59use crate::frontend::api::ProjectId;
60use crate::frontend::api::RestoreSketchCheckpointOutcome;
61use crate::frontend::api::SceneGraph;
62use crate::frontend::api::SceneGraphDelta;
63use crate::frontend::api::SketchCheckpointId;
64use crate::frontend::api::SourceDelta;
65use crate::frontend::api::SourceRef;
66use crate::frontend::api::SourceRefRange;
67use crate::frontend::api::Version;
68use crate::frontend::api::WallSource;
69use crate::frontend::modify::find_defined_names;
70use crate::frontend::modify::next_free_name;
71use crate::frontend::modify::next_free_name_with_padding;
72use crate::frontend::sketch::Coincident;
73use crate::frontend::sketch::Constraint;
74use crate::frontend::sketch::ConstraintSegment;
75use crate::frontend::sketch::Diameter;
76use crate::frontend::sketch::ExistingSegmentCtor;
77use crate::frontend::sketch::Horizontal;
78use crate::frontend::sketch::LineCtor;
79use crate::frontend::sketch::Point2d;
80use crate::frontend::sketch::Radius;
81use crate::frontend::sketch::Segment;
82use crate::frontend::sketch::SegmentCtor;
83use crate::frontend::sketch::SketchApi;
84use crate::frontend::sketch::SketchCtor;
85use crate::frontend::sketch::Vertical;
86use crate::frontend::traverse::MutateBodyItem;
87use crate::frontend::traverse::TraversalReturn;
88use crate::frontend::traverse::Visitor;
89use crate::frontend::traverse::dfs_mut;
90use crate::id::IncIdGenerator;
91use crate::parsing::ast::types as ast;
92use crate::parsing::ast::types::NodePathExt;
93use crate::pretty::NumericSuffix;
94use crate::std::constraints::LinesAtAngleKind;
95use crate::walk::NodeMut;
96use crate::walk::Visitable;
97
98pub(crate) mod api;
99pub(crate) mod modify;
100pub(crate) mod sketch;
101
102pub const MAX_SKETCH_CHECKPOINTS: usize = 100;
103
104#[derive(Debug, Clone)]
105struct SketchCheckpoint {
106 id: SketchCheckpointId,
107 source: SourceDelta,
108 program: Program,
109 scene_graph: SceneGraph,
110 exec_outcome: ExecOutcome,
111 point_freedom_cache: HashMap<ObjectId, Freedom>,
112 mock_memory: Option<SketchModeState>,
113}
114mod traverse;
115pub(crate) mod trim;
116
117struct ArcSizeConstraintParams {
118 points: Vec<ObjectId>,
119 function_name: &'static str,
120 value: f64,
121 units: NumericSuffix,
122 label_position: Option<Point2d<Number>>,
123 constraint_type_name: &'static str,
124}
125
126const POINT_FN: &str = "point";
127const POINT_AT_PARAM: &str = "at";
128const LINE_FN: &str = "line";
129const LINE_VARIABLE: &str = "line";
130const LINE_START_PARAM: &str = "start";
131const LINE_END_PARAM: &str = "end";
132const ARC_FN: &str = "arc";
133const ARC_VARIABLE: &str = "arc";
134const ARC_START_PARAM: &str = "start";
135const ARC_END_PARAM: &str = "end";
136const ARC_CENTER_PARAM: &str = "center";
137const CIRCLE_FN: &str = "circle";
138const CIRCLE_VARIABLE: &str = "circle";
139const CIRCLE_START_PARAM: &str = "start";
140const CIRCLE_CENTER_PARAM: &str = "center";
141const CONTROL_POINT_SPLINE_FN: &str = "controlPointSpline";
142const CONTROL_POINT_SPLINE_POINTS_PARAM: &str = "points";
143const LABEL_POSITION_PARAM: &str = "labelPosition";
144
145const COINCIDENT_FN: &str = "coincident";
146const DIAMETER_FN: &str = "diameter";
147const DISTANCE_FN: &str = "distance";
148const FIXED_FN: &str = "fixed";
149const ANGLE_FN: &str = "angle";
150const HORIZONTAL_DISTANCE_FN: &str = "horizontalDistance";
151const VERTICAL_DISTANCE_FN: &str = "verticalDistance";
152const EQUAL_LENGTH_FN: &str = "equalLength";
153const EQUAL_RADIUS_FN: &str = "equalRadius";
154const HORIZONTAL_FN: &str = "horizontal";
155const MIDPOINT_FN: &str = "midpoint";
156const MIDPOINT_POINT_PARAM: &str = "point";
157const RADIUS_FN: &str = "radius";
158const SYMMETRIC_FN: &str = "symmetric";
159const SYMMETRIC_AXIS_PARAM: &str = "axis";
160const TANGENT_FN: &str = "tangent";
161const VERTICAL_FN: &str = "vertical";
162
163const LINE_PROPERTY_START: &str = "start";
164const LINE_PROPERTY_END: &str = "end";
165
166const ARC_PROPERTY_START: &str = "start";
167const ARC_PROPERTY_END: &str = "end";
168const ARC_PROPERTY_CENTER: &str = "center";
169const CIRCLE_PROPERTY_START: &str = "start";
170const CIRCLE_PROPERTY_CENTER: &str = "center";
171const CONTROL_POINT_SPLINE_PROPERTY_CONTROLS: &str = "controls";
172const CONTROL_POINT_SPLINE_PROPERTY_EDGES: &str = "edges";
173
174const CONSTRUCTION_PARAM: &str = "construction";
175
176#[derive(Debug, Clone, Copy)]
177enum EditDeleteKind {
178 Edit,
179 DeleteNonSketch,
180}
181
182struct ExecuteAfterEditOptions {
184 segment_ids_edited: AhashIndexSet<ObjectId>,
185 edit_kind: EditDeleteKind,
186 commit_solved_initial_guesses: bool,
187}
188
189impl EditDeleteKind {
190 fn is_delete(&self) -> bool {
192 match self {
193 EditDeleteKind::Edit => false,
194 EditDeleteKind::DeleteNonSketch => true,
195 }
196 }
197
198 fn to_change_kind(self) -> ChangeKind {
199 match self {
200 EditDeleteKind::Edit => ChangeKind::Edit,
201 EditDeleteKind::DeleteNonSketch => ChangeKind::Delete,
202 }
203 }
204}
205
206#[derive(Debug, Clone, Copy)]
207enum ChangeKind {
208 Add,
209 Edit,
210 Delete,
211 None,
212}
213
214#[derive(Debug, Clone, Serialize, ts_rs::TS)]
215#[ts(export, export_to = "FrontendApi.ts")]
216#[serde(tag = "type")]
217pub enum SetProgramOutcome {
218 #[serde(rename_all = "camelCase")]
219 Success {
220 scene_graph: Box<SceneGraph>,
221 exec_outcome: Box<ExecOutcome>,
222 checkpoint_id: Option<SketchCheckpointId>,
223 },
224 #[serde(rename_all = "camelCase")]
225 ExecFailure { error: Box<KclErrorWithOutputs> },
226}
227
228pub struct EditSegmentsOptions {
230 pub anchor_segment_ids: Option<Vec<ObjectId>>,
236 pub drag_anchors: Vec<SegmentDragAnchor>,
239 pub commit_solved_initial_guesses: bool,
241}
242
243pub struct EditDistanceConstraintLabelPositionOptions {
245 pub anchor_segment_ids: Vec<ObjectId>,
247 pub commit_solved_initial_guesses: bool,
249}
250
251#[derive(Debug, Clone)]
252pub struct FrontendState {
253 program: Program,
254 scene_graph: SceneGraph,
255 point_freedom_cache: HashMap<ObjectId, Freedom>,
258 next_drag_anchor_segment_ids: Option<AhashIndexSet<ObjectId>>,
261 next_segment_drag_anchors: Option<Vec<SegmentDragAnchor>>,
264 next_edit_commits_solver_solutions: Option<bool>,
268 sketch_checkpoints: VecDeque<SketchCheckpoint>,
269 sketch_checkpoint_id_gen: IncIdGenerator<u64>,
270}
271
272impl Default for FrontendState {
273 fn default() -> Self {
274 Self::new()
275 }
276}
277
278impl FrontendState {
279 pub fn new() -> Self {
280 Self {
281 program: Program::empty(),
282 scene_graph: SceneGraph {
283 project: ProjectId(0),
284 file: FileId(0),
285 version: Version(0),
286 objects: Default::default(),
287 settings: Default::default(),
288 sketch_mode: Default::default(),
289 },
290 point_freedom_cache: HashMap::new(),
291 next_drag_anchor_segment_ids: None,
292 next_segment_drag_anchors: None,
293 next_edit_commits_solver_solutions: None,
294 sketch_checkpoints: VecDeque::new(),
295 sketch_checkpoint_id_gen: IncIdGenerator::new(1),
296 }
297 }
298
299 pub fn scene_graph(&self) -> &SceneGraph {
301 &self.scene_graph
302 }
303
304 pub fn default_length_unit(&self) -> UnitLength {
305 self.program
306 .meta_settings()
307 .ok()
308 .flatten()
309 .map(|settings| settings.default_length_units)
310 .unwrap_or(UnitLength::Millimeters)
311 }
312
313 pub async fn create_sketch_checkpoint(&mut self, exec_outcome: ExecOutcome) -> api::Result<SketchCheckpointId> {
314 let checkpoint_id = SketchCheckpointId::new(self.sketch_checkpoint_id_gen.next_id());
315
316 let checkpoint = SketchCheckpoint {
317 id: checkpoint_id,
318 source: SourceDelta {
319 text: source_from_ast(&self.program.ast),
320 },
321 program: self.program.clone(),
322 scene_graph: self.scene_graph.clone(),
323 exec_outcome,
324 point_freedom_cache: self.point_freedom_cache.clone(),
325 mock_memory: read_old_memory().await,
326 };
327
328 self.sketch_checkpoints.push_back(checkpoint);
329 while self.sketch_checkpoints.len() > MAX_SKETCH_CHECKPOINTS {
330 self.sketch_checkpoints.pop_front();
331 }
332
333 Ok(checkpoint_id)
334 }
335
336 pub async fn edit_segments_with_options(
344 &mut self,
345 ctx: &ExecutorContext,
346 version: Version,
347 sketch: ObjectId,
348 segments: Vec<ExistingSegmentCtor>,
349 options: EditSegmentsOptions,
350 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
351 let previous_anchor_ids = options.anchor_segment_ids.map(|anchor_ids| {
352 self.next_drag_anchor_segment_ids
353 .replace(anchor_ids.into_iter().collect())
354 });
355 let previous_drag_anchors = self.next_segment_drag_anchors.replace(options.drag_anchors);
356 let previous_commit_mode = self
357 .next_edit_commits_solver_solutions
358 .replace(options.commit_solved_initial_guesses);
359 let result = SketchApi::edit_segments(self, ctx, version, sketch, segments).await;
360 if let Some(previous_anchor_ids) = previous_anchor_ids {
361 self.next_drag_anchor_segment_ids = previous_anchor_ids;
362 }
363 self.next_segment_drag_anchors = previous_drag_anchors;
364 self.next_edit_commits_solver_solutions = previous_commit_mode;
365 result
366 }
367
368 pub async fn edit_distance_constraint_label_position_with_options(
374 &mut self,
375 ctx: &ExecutorContext,
376 version: Version,
377 sketch: ObjectId,
378 constraint_id: ObjectId,
379 label_position: Point2d<Number>,
380 options: EditDistanceConstraintLabelPositionOptions,
381 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
382 let previous_commit_mode = self
383 .next_edit_commits_solver_solutions
384 .replace(options.commit_solved_initial_guesses);
385 let result = SketchApi::edit_distance_constraint_label_position(
386 self,
387 ctx,
388 version,
389 sketch,
390 constraint_id,
391 label_position,
392 options.anchor_segment_ids,
393 )
394 .await;
395 self.next_edit_commits_solver_solutions = previous_commit_mode;
396 result
397 }
398
399 pub async fn restore_sketch_checkpoint(
400 &mut self,
401 checkpoint_id: SketchCheckpointId,
402 ) -> api::Result<RestoreSketchCheckpointOutcome> {
403 let checkpoint = self
404 .sketch_checkpoints
405 .iter()
406 .find(|checkpoint| checkpoint.id == checkpoint_id)
407 .cloned()
408 .ok_or_else(|| Error {
409 msg: format!("Sketch checkpoint not found: {checkpoint_id:?}"),
410 })?;
411
412 self.program = checkpoint.program;
413 self.scene_graph = checkpoint.scene_graph.clone();
414 self.point_freedom_cache = checkpoint.point_freedom_cache;
415 self.next_drag_anchor_segment_ids = None;
416 self.next_segment_drag_anchors = None;
417 self.next_edit_commits_solver_solutions = None;
418
419 if let Some(mock_memory) = checkpoint.mock_memory {
420 write_old_memory(mock_memory).await;
421 } else {
422 clear_mem_cache().await;
423 }
424
425 Ok(RestoreSketchCheckpointOutcome {
426 source_delta: checkpoint.source,
427 scene_graph_delta: SceneGraphDelta {
428 new_graph: self.scene_graph_for_ui(),
429 new_objects: Vec::new(),
430 invalidates_ids: true,
431 exec_outcome: checkpoint.exec_outcome,
432 },
433 })
434 }
435
436 pub fn clear_sketch_checkpoints(&mut self) {
437 self.sketch_checkpoints.clear();
438 }
439 fn scene_graph_for_ui(&self) -> SceneGraph {
440 let has_control_point_splines = self.scene_graph.objects.iter().any(|object| {
441 matches!(
442 object.kind,
443 ObjectKind::Segment {
444 segment: Segment::ControlPointSpline(_)
445 }
446 )
447 });
448
449 if !has_control_point_splines {
450 return self.scene_graph.clone();
451 }
452
453 let hidden_constraint_ids = self
454 .scene_graph
455 .objects
456 .iter()
457 .filter_map(|object| match &object.kind {
458 ObjectKind::Constraint {
459 constraint: Constraint::Coincident(coincident),
460 } if coincident_is_internal_to_same_control_point_spline(coincident, &self.scene_graph) => {
461 Some(object.id)
462 }
463 _ => None,
464 })
465 .collect::<HashSet<_>>();
466
467 if hidden_constraint_ids.is_empty() {
468 return self.scene_graph.clone();
469 }
470
471 let mut scene_graph = self.scene_graph.clone();
472 for object in &mut scene_graph.objects {
473 match &mut object.kind {
474 ObjectKind::Constraint { .. } if hidden_constraint_ids.contains(&object.id) => {
475 object.kind = ObjectKind::Nil;
476 }
477 ObjectKind::Sketch(sketch) => {
478 sketch
479 .constraints
480 .retain(|constraint_id| !hidden_constraint_ids.contains(constraint_id));
481 }
482 _ => {}
483 }
484 }
485
486 scene_graph
487 }
488}
489
490fn coincident_is_internal_to_same_control_point_spline(coincident: &Coincident, scene_graph: &SceneGraph) -> bool {
491 let mut first_owner_id = None;
492 for segment_id in coincident.segment_ids() {
493 let Some(owner_id) = owning_control_point_spline_id(segment_id, scene_graph) else {
494 return false;
495 };
496
497 match first_owner_id {
498 Some(first_owner_id) if first_owner_id != owner_id => return false,
499 Some(_) => {}
500 None => first_owner_id = Some(owner_id),
501 }
502 }
503
504 first_owner_id.is_some()
505}
506
507fn owning_control_point_spline_id(segment_id: ObjectId, scene_graph: &SceneGraph) -> Option<ObjectId> {
508 let object = scene_graph.objects.get(segment_id.0)?;
509 let ObjectKind::Segment { segment } = &object.kind else {
510 return None;
511 };
512
513 match segment {
514 Segment::ControlPointSpline(_) => Some(segment_id),
515 Segment::Point(point) => point
516 .owner
517 .filter(|owner_id| matches_control_point_spline_owner(*owner_id, scene_graph)),
518 Segment::Line(line) => line
519 .owner
520 .filter(|owner_id| matches_control_point_spline_owner(*owner_id, scene_graph)),
521 _ => None,
522 }
523}
524
525fn matches_control_point_spline_owner(owner_id: ObjectId, scene_graph: &SceneGraph) -> bool {
526 matches!(
527 scene_graph.objects.get(owner_id.0).map(|object| &object.kind),
528 Some(ObjectKind::Segment {
529 segment: Segment::ControlPointSpline(_)
530 })
531 )
532}
533
534fn ensure_control_point_spline_experimental_features(program: &Program) -> Result<Program, KclError> {
535 let experimental_features_allowed = program
536 .meta_settings()
537 .ok()
538 .flatten()
539 .map(|settings| settings.experimental_features == WarningLevel::Allow)
540 .unwrap_or(false);
541 if experimental_features_allowed {
542 return Ok(program.clone());
543 }
544
545 program.change_experimental_features(Some(WarningLevel::Allow))
546}
547
548impl SketchApi for FrontendState {
549 async fn execute_mock(
550 &mut self,
551 ctx: &ExecutorContext,
552 _version: Version,
553 sketch: ObjectId,
554 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
555 let sketch_block_ref =
556 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
557
558 let mut truncated_program = self.program.clone();
559 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::None)
560 .map_err(KclErrorWithOutputs::no_outputs)?;
561
562 let outcome = ctx
564 .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch))
565 .await?;
566 let new_source = source_from_ast(&self.program.ast);
567 let src_delta = SourceDelta { text: new_source };
568 let outcome = self.update_state_after_exec(outcome, true);
570 let scene_graph_delta = SceneGraphDelta {
571 new_graph: self.scene_graph.clone(),
572 new_objects: Default::default(),
573 invalidates_ids: false,
574 exec_outcome: outcome,
575 };
576 Ok((src_delta, scene_graph_delta))
577 }
578
579 async fn new_sketch(
580 &mut self,
581 ctx: &ExecutorContext,
582 _project: ProjectId,
583 _file: FileId,
584 _version: Version,
585 args: SketchCtor,
586 ) -> ExecResult<(SourceDelta, SceneGraphDelta, ObjectId)> {
587 let mut new_ast = self.program.ast.clone();
590 let mut plane_ast =
592 sketch_on_ast_expr(&mut new_ast, &self.scene_graph, &args.on).map_err(KclErrorWithOutputs::no_outputs)?;
593 let mut defined_names = find_defined_names(&new_ast);
594 let is_face_of_expr = matches!(
595 &plane_ast,
596 ast::Expr::CallExpressionKw(call) if call.callee.name.name == "faceOf"
597 );
598 if is_face_of_expr {
599 let face_name = next_free_name_with_padding("face", &defined_names)
600 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
601 let face_decl = ast::VariableDeclaration::new(
602 ast::VariableDeclarator::new(&face_name, plane_ast),
603 ast::ItemVisibility::Default,
604 ast::VariableKind::Const,
605 );
606 new_ast
607 .body
608 .push(ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(
609 face_decl,
610 ))));
611 defined_names.insert(face_name.clone());
612 plane_ast = ast::Expr::Name(Box::new(ast::Name::new(&face_name)));
613 }
614 let sketch_ast = ast::SketchBlock {
615 arguments: vec![ast::LabeledArg {
616 label: Some(ast::Identifier::new(SKETCH_BLOCK_PARAM_ON)),
617 arg: plane_ast,
618 }],
619 body: Default::default(),
620 is_being_edited: false,
621 non_code_meta: Default::default(),
622 digest: None,
623 };
624 let sketch_name = next_free_name_with_padding("sketch", &defined_names)
627 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
628 let sketch_decl = ast::VariableDeclaration::new(
629 ast::VariableDeclarator::new(
630 &sketch_name,
631 ast::Expr::SketchBlock(Box::new(ast::Node::no_src(sketch_ast))),
632 ),
633 ast::ItemVisibility::Default,
634 ast::VariableKind::Const,
635 );
636 new_ast
637 .body
638 .push(ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(
639 sketch_decl,
640 ))));
641 let new_source = source_from_ast(&new_ast);
643 let new_program = parse_frontend_mutation_source(
645 &new_source,
646 "Error parsing KCL source after adding sketch",
647 "No AST produced after adding sketch",
648 )?;
649
650 self.program = new_program.clone();
652
653 let outcome = ctx.run_with_caching(new_program.clone()).await?;
656 let freedom_analysis_ran = true;
657
658 let outcome = self.update_state_after_exec(outcome, freedom_analysis_ran);
659
660 let Some(sketch_id) = self
661 .scene_graph
662 .objects
663 .iter()
664 .filter_map(|object| match object.kind {
665 ObjectKind::Sketch(_) => Some(object.id),
666 _ => None,
667 })
668 .max_by_key(|id| id.0)
669 else {
670 return Err(KclErrorWithOutputs::from_error_outcome(
671 KclError::refactor("No objects in scene graph after adding sketch".to_owned()),
672 outcome,
673 ));
674 };
675 self.scene_graph.sketch_mode = Some(sketch_id);
677
678 let src_delta = SourceDelta { text: new_source };
679 let scene_graph_delta = SceneGraphDelta {
680 new_graph: self.scene_graph_for_ui(),
681 invalidates_ids: false,
682 new_objects: vec![sketch_id],
683 exec_outcome: outcome,
684 };
685 Ok((src_delta, scene_graph_delta, sketch_id))
686 }
687
688 async fn edit_sketch(
689 &mut self,
690 ctx: &ExecutorContext,
691 _project: ProjectId,
692 _file: FileId,
693 _version: Version,
694 sketch: ObjectId,
695 ) -> ExecResult<SceneGraphDelta> {
696 let sketch_object = self.scene_graph.objects.get(sketch.0).ok_or_else(|| {
700 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
701 })?;
702 let ObjectKind::Sketch(_) = &sketch_object.kind else {
703 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
704 "Object is not a sketch, it is {}",
705 sketch_object.kind.human_friendly_kind_with_article()
706 ))));
707 };
708 let sketch_block_ref = expect_single_node_ref(sketch_object).map_err(KclErrorWithOutputs::no_outputs)?;
709
710 self.scene_graph.sketch_mode = Some(sketch);
712
713 let mut truncated_program = self.program.clone();
715 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::None)
716 .map_err(KclErrorWithOutputs::no_outputs)?;
717
718 let outcome = ctx
721 .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch))
722 .await?;
723
724 let outcome = self.update_state_after_exec(outcome, true);
726 let scene_graph_delta = SceneGraphDelta {
727 new_graph: self.scene_graph_for_ui(),
728 invalidates_ids: false,
729 new_objects: Vec::new(),
730 exec_outcome: outcome,
731 };
732 Ok(scene_graph_delta)
733 }
734
735 async fn exit_sketch(
736 &mut self,
737 ctx: &ExecutorContext,
738 _version: Version,
739 sketch: ObjectId,
740 ) -> ExecResult<SceneGraph> {
741 #[cfg(not(target_arch = "wasm32"))]
743 let _ = sketch;
744 #[cfg(target_arch = "wasm32")]
745 if self.scene_graph.sketch_mode != Some(sketch) {
746 web_sys::console::warn_1(
747 &format!(
748 "WARNING: exit_sketch: current state's sketch mode ID doesn't match the given sketch ID; state={:#?}, given={sketch:?}",
749 self.scene_graph.sketch_mode
750 )
751 .into(),
752 );
753 }
754 self.scene_graph.sketch_mode = None;
755
756 let outcome = ctx.run_with_caching(self.program.clone()).await?;
758
759 self.update_state_after_exec(outcome, false);
761
762 Ok(self.scene_graph_for_ui())
763 }
764
765 async fn delete_sketch(
766 &mut self,
767 ctx: &ExecutorContext,
768 _version: Version,
769 sketch: ObjectId,
770 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
771 let mut new_ast = self.program.ast.clone();
774
775 let sketch_id = sketch;
777 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
778 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
779 })?;
780 let ObjectKind::Sketch(_) = &sketch_object.kind else {
781 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
782 "Object is not a sketch, it is {}",
783 sketch_object.kind.human_friendly_kind_with_article(),
784 ))));
785 };
786
787 self.mutate_ast(&mut new_ast, sketch_id, AstMutateCommand::DeleteNode)
789 .map_err(KclErrorWithOutputs::no_outputs)?;
790
791 self.execute_after_delete_sketch(ctx, &mut new_ast).await
792 }
793
794 async fn add_segment(
795 &mut self,
796 ctx: &ExecutorContext,
797 _version: Version,
798 sketch: ObjectId,
799 segment: SegmentCtor,
800 _label: Option<String>,
801 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
802 match segment {
804 SegmentCtor::Point(ctor) => self.add_point(ctx, sketch, ctor).await,
805 SegmentCtor::Line(ctor) => self.add_line(ctx, sketch, ctor).await,
806 SegmentCtor::Arc(ctor) => self.add_arc(ctx, sketch, ctor).await,
807 SegmentCtor::Circle(ctor) => self.add_circle(ctx, sketch, ctor).await,
808 SegmentCtor::ControlPointSpline(ctor) => self.add_control_point_spline(ctx, sketch, ctor).await,
809 }
810 }
811
812 async fn edit_segments(
813 &mut self,
814 ctx: &ExecutorContext,
815 _version: Version,
816 sketch: ObjectId,
817 segments: Vec<ExistingSegmentCtor>,
818 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
819 let sketch_block_ref =
821 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
822
823 let mut new_ast = self.program.ast.clone();
824 let mut edited_segment_ids = AhashIndexSet::with_capacity_and_hasher(segments.len(), Default::default());
825 let mut invalidates_ids = false;
826
827 for segment in &segments {
830 edited_segment_ids.insert(segment.id);
831 if let SegmentCtor::ControlPointSpline(new_ctor) = &segment.ctor
832 && let Some(existing_object) = self.scene_graph.objects.get(segment.id.0)
833 && let ObjectKind::Segment {
834 segment: Segment::ControlPointSpline(existing_spline),
835 } = &existing_object.kind
836 && existing_spline.controls.len() != new_ctor.points.len()
837 {
838 invalidates_ids = true;
839 }
840 }
841 let drag_anchor_segment_ids = self
842 .next_drag_anchor_segment_ids
843 .take()
844 .unwrap_or_else(|| edited_segment_ids.clone());
845 let commit_solved_initial_guesses = self.next_edit_commits_solver_solutions.take().unwrap_or(true);
846
847 let mut final_edits: IndexMap<ObjectId, SegmentCtor> = IndexMap::new();
862
863 for segment in segments {
864 let segment_id = segment.id;
865 match segment.ctor {
866 SegmentCtor::Point(ctor) => {
867 if let Some(segment_object) = self.scene_graph.objects.get(segment_id.0)
869 && let ObjectKind::Segment { segment } = &segment_object.kind
870 && let Segment::Point(point) = segment
871 && let Some(owner_id) = point.owner
872 && let Some(owner_object) = self.scene_graph.objects.get(owner_id.0)
873 && let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind
874 {
875 match owner_segment {
876 Segment::Line(line) if line.start == segment_id || line.end == segment_id => {
877 if let Some(existing) = final_edits.get_mut(&owner_id) {
878 let SegmentCtor::Line(line_ctor) = existing else {
879 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
880 "Internal: Expected line ctor for owner, but found {}",
881 existing.human_friendly_kind_with_article()
882 ))));
883 };
884 if line.start == segment_id {
886 line_ctor.start = ctor.position;
887 } else {
888 line_ctor.end = ctor.position;
889 }
890 } else if let SegmentCtor::Line(line_ctor) = &line.ctor {
891 let mut line_ctor = line_ctor.clone();
893 if line.start == segment_id {
894 line_ctor.start = ctor.position;
895 } else {
896 line_ctor.end = ctor.position;
897 }
898 final_edits.insert(owner_id, SegmentCtor::Line(line_ctor));
899 } else {
900 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
902 "Internal: Line does not have line ctor, but found {}",
903 line.ctor.human_friendly_kind_with_article()
904 ))));
905 }
906 continue;
907 }
908 Segment::Arc(arc)
909 if arc.start == segment_id || arc.end == segment_id || arc.center == segment_id =>
910 {
911 if let Some(existing) = final_edits.get_mut(&owner_id) {
912 let SegmentCtor::Arc(arc_ctor) = existing else {
913 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
914 "Internal: Expected arc ctor for owner, but found {}",
915 existing.human_friendly_kind_with_article()
916 ))));
917 };
918 if arc.start == segment_id {
919 arc_ctor.start = ctor.position;
920 } else if arc.end == segment_id {
921 arc_ctor.end = ctor.position;
922 } else {
923 arc_ctor.center = ctor.position;
924 }
925 } else if let SegmentCtor::Arc(arc_ctor) = &arc.ctor {
926 let mut arc_ctor = arc_ctor.clone();
927 if arc.start == segment_id {
928 arc_ctor.start = ctor.position;
929 } else if arc.end == segment_id {
930 arc_ctor.end = ctor.position;
931 } else {
932 arc_ctor.center = ctor.position;
933 }
934 final_edits.insert(owner_id, SegmentCtor::Arc(arc_ctor));
935 } else {
936 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
937 "Internal: Arc does not have arc ctor, but found {}",
938 arc.ctor.human_friendly_kind_with_article()
939 ))));
940 }
941 continue;
942 }
943 Segment::Circle(circle) if circle.start == segment_id || circle.center == segment_id => {
944 if let Some(existing) = final_edits.get_mut(&owner_id) {
945 let SegmentCtor::Circle(circle_ctor) = existing else {
946 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
947 "Internal: Expected circle ctor for owner, but found {}",
948 existing.human_friendly_kind_with_article()
949 ))));
950 };
951 if circle.start == segment_id {
952 circle_ctor.start = ctor.position;
953 } else {
954 circle_ctor.center = ctor.position;
955 }
956 } else if let SegmentCtor::Circle(circle_ctor) = &circle.ctor {
957 let mut circle_ctor = circle_ctor.clone();
958 if circle.start == segment_id {
959 circle_ctor.start = ctor.position;
960 } else {
961 circle_ctor.center = ctor.position;
962 }
963 final_edits.insert(owner_id, SegmentCtor::Circle(circle_ctor));
964 } else {
965 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
966 "Internal: Circle does not have circle ctor, but found {}",
967 circle.ctor.human_friendly_kind_with_article()
968 ))));
969 }
970 continue;
971 }
972 Segment::ControlPointSpline(spline) if spline.controls.contains(&segment_id) => {
973 let Some(control_index) =
974 spline.controls.iter().position(|control_id| *control_id == segment_id)
975 else {
976 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
977 "Internal: Point is not part of owner's controlPointSpline segment: point={segment_id:?}, spline={owner_id:?}"
978 ))));
979 };
980 if let Some(existing) = final_edits.get_mut(&owner_id) {
981 let SegmentCtor::ControlPointSpline(spline_ctor) = existing else {
982 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
983 "Internal: Expected controlPointSpline ctor for owner, but found {}",
984 existing.human_friendly_kind_with_article()
985 ))));
986 };
987 spline_ctor.points[control_index] = ctor.position;
988 } else if let SegmentCtor::ControlPointSpline(spline_ctor) = &spline.ctor {
989 let mut spline_ctor = spline_ctor.clone();
990 spline_ctor.points[control_index] = ctor.position;
991 final_edits.insert(owner_id, SegmentCtor::ControlPointSpline(spline_ctor));
992 } else {
993 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
994 "Internal: Control point spline does not have controlPointSpline ctor, but found {}",
995 spline.ctor.human_friendly_kind_with_article()
996 ))));
997 }
998 continue;
999 }
1000 _ => {}
1001 }
1002 }
1003
1004 final_edits.insert(segment_id, SegmentCtor::Point(ctor));
1006 }
1007 SegmentCtor::Line(ctor) => {
1008 final_edits.insert(segment_id, SegmentCtor::Line(ctor));
1009 }
1010 SegmentCtor::Arc(ctor) => {
1011 final_edits.insert(segment_id, SegmentCtor::Arc(ctor));
1012 }
1013 SegmentCtor::Circle(ctor) => {
1014 final_edits.insert(segment_id, SegmentCtor::Circle(ctor));
1015 }
1016 SegmentCtor::ControlPointSpline(ctor) => {
1017 final_edits.insert(segment_id, SegmentCtor::ControlPointSpline(ctor));
1018 }
1019 }
1020 }
1021
1022 for (segment_id, ctor) in final_edits {
1023 match ctor {
1024 SegmentCtor::Point(ctor) => self
1025 .edit_point(&mut new_ast, sketch, segment_id, ctor)
1026 .map_err(KclErrorWithOutputs::no_outputs)?,
1027 SegmentCtor::Line(ctor) => self
1028 .edit_line(&mut new_ast, sketch, segment_id, ctor)
1029 .map_err(KclErrorWithOutputs::no_outputs)?,
1030 SegmentCtor::Arc(ctor) => self
1031 .edit_arc(&mut new_ast, sketch, segment_id, ctor)
1032 .map_err(KclErrorWithOutputs::no_outputs)?,
1033 SegmentCtor::Circle(ctor) => self
1034 .edit_circle(&mut new_ast, sketch, segment_id, ctor)
1035 .map_err(KclErrorWithOutputs::no_outputs)?,
1036 SegmentCtor::ControlPointSpline(ctor) => self
1037 .edit_control_point_spline(&mut new_ast, sketch, segment_id, ctor)
1038 .map_err(KclErrorWithOutputs::no_outputs)?,
1039 }
1040 }
1041 let (source_delta, mut scene_graph_delta) = self
1042 .execute_after_edit(
1043 ctx,
1044 sketch,
1045 sketch_block_ref,
1046 &mut new_ast,
1047 ExecuteAfterEditOptions {
1048 segment_ids_edited: drag_anchor_segment_ids,
1049 edit_kind: EditDeleteKind::Edit,
1050 commit_solved_initial_guesses,
1051 },
1052 )
1053 .await?;
1054 if invalidates_ids {
1055 scene_graph_delta.invalidates_ids = true;
1056 }
1057 Ok((source_delta, scene_graph_delta))
1058 }
1059
1060 async fn delete_objects(
1061 &mut self,
1062 ctx: &ExecutorContext,
1063 _version: Version,
1064 sketch: ObjectId,
1065 constraint_ids: Vec<ObjectId>,
1066 segment_ids: Vec<ObjectId>,
1067 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1068 let sketch_block_ref =
1070 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1071
1072 let mut constraint_ids_set = constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
1074 let segment_ids_set = segment_ids.into_iter().collect::<AhashIndexSet<_>>();
1075
1076 let mut resolved_segment_ids_to_delete = AhashIndexSet::default();
1079
1080 for segment_id in segment_ids_set.iter().copied() {
1081 let owner_id = self.scene_graph.objects.get(segment_id.0).and_then(|segment_object| {
1082 let ObjectKind::Segment { segment } = &segment_object.kind else {
1083 return None;
1084 };
1085 match segment {
1086 Segment::Point(point) => point.owner,
1087 Segment::Line(line) => line.owner,
1088 _ => None,
1089 }
1090 });
1091
1092 if let Some(owner_id) = owner_id
1093 && let Some(owner_object) = self.scene_graph.objects.get(owner_id.0)
1094 && let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind
1095 && matches!(
1096 owner_segment,
1097 Segment::Line(_) | Segment::Arc(_) | Segment::Circle(_) | Segment::ControlPointSpline(_)
1098 )
1099 {
1100 resolved_segment_ids_to_delete.insert(owner_id);
1102 } else {
1103 resolved_segment_ids_to_delete.insert(segment_id);
1105 }
1106 }
1107 let referenced_constraint_ids = self
1108 .find_referenced_constraints(sketch, &resolved_segment_ids_to_delete)
1109 .map_err(KclErrorWithOutputs::no_outputs)?;
1110
1111 let mut new_ast = self.program.ast.clone();
1112
1113 for constraint_id in referenced_constraint_ids {
1114 if constraint_ids_set.contains(&constraint_id) {
1115 continue;
1116 }
1117
1118 let constraint_object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
1119 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Constraint not found: {constraint_id:?}")))
1120 })?;
1121 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
1122 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1123 "Object is not a constraint, it is {}",
1124 constraint_object.kind.human_friendly_kind_with_article()
1125 ))));
1126 };
1127
1128 match constraint {
1129 Constraint::Coincident(coincident) => {
1130 let remaining_segments =
1131 self.remaining_constraint_segments(&coincident.segments, &resolved_segment_ids_to_delete);
1132
1133 if remaining_segments.len() >= 2 {
1135 self.edit_coincident_constraint(&mut new_ast, constraint_id, remaining_segments)
1136 .map_err(KclErrorWithOutputs::no_outputs)?;
1137 } else {
1138 constraint_ids_set.insert(constraint_id);
1139 }
1140 }
1141 Constraint::EqualRadius(equal_radius) => {
1142 let remaining_input = equal_radius
1143 .input
1144 .iter()
1145 .copied()
1146 .filter(|segment_id| {
1147 !self.segment_will_be_deleted(*segment_id, &resolved_segment_ids_to_delete)
1148 })
1149 .collect::<Vec<_>>();
1150
1151 if remaining_input.len() >= 2 {
1152 self.edit_equal_radius_constraint(&mut new_ast, constraint_id, remaining_input)
1153 .map_err(KclErrorWithOutputs::no_outputs)?;
1154 } else {
1155 constraint_ids_set.insert(constraint_id);
1156 }
1157 }
1158 Constraint::LinesEqualLength(lines_equal_length) => {
1159 let remaining_lines = lines_equal_length
1160 .lines
1161 .iter()
1162 .copied()
1163 .filter(|line_id| !self.segment_will_be_deleted(*line_id, &resolved_segment_ids_to_delete))
1164 .collect::<Vec<_>>();
1165
1166 if remaining_lines.len() >= 2 {
1168 self.edit_equal_length_constraint(&mut new_ast, constraint_id, remaining_lines)
1169 .map_err(KclErrorWithOutputs::no_outputs)?;
1170 } else {
1171 constraint_ids_set.insert(constraint_id);
1172 }
1173 }
1174 Constraint::Parallel(parallel) => {
1175 let remaining_lines = parallel
1176 .lines
1177 .iter()
1178 .copied()
1179 .filter(|line_id| !self.segment_will_be_deleted(*line_id, &resolved_segment_ids_to_delete))
1180 .collect::<Vec<_>>();
1181
1182 if remaining_lines.len() >= 2 {
1183 self.edit_parallel_constraint(&mut new_ast, constraint_id, remaining_lines)
1184 .map_err(KclErrorWithOutputs::no_outputs)?;
1185 } else {
1186 constraint_ids_set.insert(constraint_id);
1187 }
1188 }
1189 Constraint::Horizontal(Horizontal::Points { points }) => {
1190 let remaining_points = self.remaining_constraint_segments(points, &resolved_segment_ids_to_delete);
1191
1192 if remaining_points.len() >= 2 {
1193 self.edit_horizontal_points_constraint(&mut new_ast, constraint_id, remaining_points)
1194 .map_err(KclErrorWithOutputs::no_outputs)?;
1195 } else {
1196 constraint_ids_set.insert(constraint_id);
1197 }
1198 }
1199 Constraint::Vertical(Vertical::Points { points }) => {
1200 let remaining_points = self.remaining_constraint_segments(points, &resolved_segment_ids_to_delete);
1201
1202 if remaining_points.len() >= 2 {
1203 self.edit_vertical_points_constraint(&mut new_ast, constraint_id, remaining_points)
1204 .map_err(KclErrorWithOutputs::no_outputs)?;
1205 } else {
1206 constraint_ids_set.insert(constraint_id);
1207 }
1208 }
1209 Constraint::Fixed(fixed) => {
1210 if fixed.points.iter().any(|fixed_point| {
1211 self.segment_will_be_deleted(fixed_point.point, &resolved_segment_ids_to_delete)
1212 }) {
1213 constraint_ids_set.insert(constraint_id);
1214 }
1215 }
1216 _ => {
1217 constraint_ids_set.insert(constraint_id);
1219 }
1220 }
1221 }
1222
1223 for constraint_id in constraint_ids_set {
1224 self.delete_constraint(&mut new_ast, sketch, constraint_id)
1225 .map_err(KclErrorWithOutputs::no_outputs)?;
1226 }
1227 for segment_id in resolved_segment_ids_to_delete {
1228 self.delete_segment(&mut new_ast, sketch, segment_id)
1229 .map_err(KclErrorWithOutputs::no_outputs)?;
1230 }
1231
1232 self.execute_after_edit(
1233 ctx,
1234 sketch,
1235 sketch_block_ref,
1236 &mut new_ast,
1237 ExecuteAfterEditOptions {
1238 segment_ids_edited: Default::default(),
1239 edit_kind: EditDeleteKind::DeleteNonSketch,
1240 commit_solved_initial_guesses: true,
1241 },
1242 )
1243 .await
1244 }
1245
1246 async fn add_constraint(
1247 &mut self,
1248 ctx: &ExecutorContext,
1249 _version: Version,
1250 sketch: ObjectId,
1251 constraint: Constraint,
1252 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1253 let original_program = self.program.clone();
1257 let original_scene_graph = self.scene_graph.clone();
1258
1259 let mut new_ast = self.program.ast.clone();
1260 let sketch_block_ref = match constraint {
1261 Constraint::Coincident(coincident) => self
1262 .add_coincident(sketch, coincident, &mut new_ast)
1263 .await
1264 .map_err(KclErrorWithOutputs::no_outputs)?,
1265 Constraint::Distance(distance) => self
1266 .add_distance(sketch, distance, &mut new_ast)
1267 .await
1268 .map_err(KclErrorWithOutputs::no_outputs)?,
1269 Constraint::EqualRadius(equal_radius) => self
1270 .add_equal_radius(sketch, equal_radius, &mut new_ast)
1271 .await
1272 .map_err(KclErrorWithOutputs::no_outputs)?,
1273 Constraint::Fixed(fixed) => self
1274 .add_fixed_constraints(sketch, fixed.points, &mut new_ast)
1275 .await
1276 .map_err(KclErrorWithOutputs::no_outputs)?,
1277 Constraint::HorizontalDistance(distance) => self
1278 .add_horizontal_distance(sketch, distance, &mut new_ast)
1279 .await
1280 .map_err(KclErrorWithOutputs::no_outputs)?,
1281 Constraint::VerticalDistance(distance) => self
1282 .add_vertical_distance(sketch, distance, &mut new_ast)
1283 .await
1284 .map_err(KclErrorWithOutputs::no_outputs)?,
1285 Constraint::Horizontal(horizontal) => self
1286 .add_horizontal(sketch, horizontal, &mut new_ast)
1287 .await
1288 .map_err(KclErrorWithOutputs::no_outputs)?,
1289 Constraint::LinesEqualLength(lines_equal_length) => self
1290 .add_lines_equal_length(sketch, lines_equal_length, &mut new_ast)
1291 .await
1292 .map_err(KclErrorWithOutputs::no_outputs)?,
1293 Constraint::Midpoint(midpoint) => self
1294 .add_midpoint(sketch, midpoint, &mut new_ast)
1295 .await
1296 .map_err(KclErrorWithOutputs::no_outputs)?,
1297 Constraint::Parallel(parallel) => self
1298 .add_parallel(sketch, parallel, &mut new_ast)
1299 .await
1300 .map_err(KclErrorWithOutputs::no_outputs)?,
1301 Constraint::Perpendicular(perpendicular) => self
1302 .add_perpendicular(sketch, perpendicular, &mut new_ast)
1303 .await
1304 .map_err(KclErrorWithOutputs::no_outputs)?,
1305 Constraint::Radius(radius) => self
1306 .add_radius(sketch, radius, &mut new_ast)
1307 .await
1308 .map_err(KclErrorWithOutputs::no_outputs)?,
1309 Constraint::Diameter(diameter) => self
1310 .add_diameter(sketch, diameter, &mut new_ast)
1311 .await
1312 .map_err(KclErrorWithOutputs::no_outputs)?,
1313 Constraint::Symmetric(symmetric) => self
1314 .add_symmetric(sketch, symmetric, &mut new_ast)
1315 .await
1316 .map_err(KclErrorWithOutputs::no_outputs)?,
1317 Constraint::Vertical(vertical) => self
1318 .add_vertical(sketch, vertical, &mut new_ast)
1319 .await
1320 .map_err(KclErrorWithOutputs::no_outputs)?,
1321 Constraint::Angle(lines_at_angle) => self
1322 .add_angle(sketch, lines_at_angle, &mut new_ast)
1323 .await
1324 .map_err(KclErrorWithOutputs::no_outputs)?,
1325 Constraint::Tangent(tangent) => self
1326 .add_tangent(sketch, tangent, &mut new_ast)
1327 .await
1328 .map_err(KclErrorWithOutputs::no_outputs)?,
1329 };
1330
1331 let result = self
1332 .execute_after_add_constraint(ctx, sketch, sketch_block_ref, &mut new_ast)
1333 .await;
1334
1335 if result.is_err() {
1337 self.program = original_program;
1338 self.scene_graph = original_scene_graph;
1339 }
1340
1341 result
1342 }
1343
1344 async fn chain_segment(
1345 &mut self,
1346 ctx: &ExecutorContext,
1347 version: Version,
1348 sketch: ObjectId,
1349 previous_segment_end_point_id: ObjectId,
1350 segment: SegmentCtor,
1351 _label: Option<String>,
1352 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1353 let SegmentCtor::Line(line_ctor) = segment else {
1357 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1358 "chain_segment currently only supports Line segments, got {}",
1359 segment.human_friendly_kind_with_article(),
1360 ))));
1361 };
1362
1363 let (_first_src_delta, first_scene_delta) = self.add_line(ctx, sketch, line_ctor).await?;
1365
1366 let new_line_id = first_scene_delta
1369 .new_objects
1370 .iter()
1371 .find(|&obj_id| {
1372 let obj = self.scene_graph.objects.get(obj_id.0);
1373 if let Some(obj) = obj {
1374 matches!(
1375 &obj.kind,
1376 ObjectKind::Segment {
1377 segment: Segment::Line(_)
1378 }
1379 )
1380 } else {
1381 false
1382 }
1383 })
1384 .ok_or_else(|| {
1385 KclErrorWithOutputs::no_outputs(KclError::refactor(
1386 "Failed to find new line segment in scene graph".to_string(),
1387 ))
1388 })?;
1389
1390 let new_line_obj = self.scene_graph.objects.get(new_line_id.0).ok_or_else(|| {
1391 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1392 "New line object not found: {new_line_id:?}"
1393 )))
1394 })?;
1395
1396 let ObjectKind::Segment {
1397 segment: new_line_segment,
1398 } = &new_line_obj.kind
1399 else {
1400 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1401 "Object is not a segment: {new_line_obj:?}"
1402 ))));
1403 };
1404
1405 let Segment::Line(new_line) = new_line_segment else {
1406 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1407 "Segment is not a line: {new_line_segment:?}"
1408 ))));
1409 };
1410
1411 let new_line_start_point_id = new_line.start;
1412
1413 let coincident = Coincident {
1415 segments: vec![previous_segment_end_point_id.into(), new_line_start_point_id.into()],
1416 };
1417
1418 let (final_src_delta, final_scene_delta) = self
1419 .add_constraint(ctx, version, sketch, Constraint::Coincident(coincident))
1420 .await?;
1421
1422 let mut combined_new_objects = first_scene_delta.new_objects.clone();
1425 combined_new_objects.extend(final_scene_delta.new_objects);
1426
1427 let scene_graph_delta = SceneGraphDelta {
1428 new_graph: self.scene_graph_for_ui(),
1429 invalidates_ids: false,
1430 new_objects: combined_new_objects,
1431 exec_outcome: final_scene_delta.exec_outcome,
1432 };
1433
1434 Ok((final_src_delta, scene_graph_delta))
1435 }
1436
1437 async fn edit_constraint(
1438 &mut self,
1439 ctx: &ExecutorContext,
1440 _version: Version,
1441 sketch: ObjectId,
1442 constraint_id: ObjectId,
1443 value_expression: String,
1444 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1445 let sketch_block_ref =
1447 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1448
1449 let object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
1450 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Object not found: {constraint_id:?}")))
1451 })?;
1452 if !matches!(&object.kind, ObjectKind::Constraint { .. }) {
1453 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1454 "Object is not a constraint: {constraint_id:?}"
1455 ))));
1456 }
1457
1458 let mut new_ast = self.program.ast.clone();
1459
1460 let (parsed, errors) = Program::parse(&value_expression).map_err(|e| {
1462 KclErrorWithOutputs::no_outputs(KclError::refactor(format_kcl_error_message(
1463 "Invalid constraint value",
1464 &e,
1465 )))
1466 })?;
1467 if !errors.is_empty() {
1468 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1469 format_compilation_issues("Invalid constraint value", &errors),
1470 )));
1471 }
1472 let mut parsed = parsed.ok_or_else(|| {
1473 KclErrorWithOutputs::no_outputs(KclError::refactor("No AST produced from value expression".to_string()))
1474 })?;
1475 if parsed.ast.body.is_empty() {
1476 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1477 "Empty value expression".to_string(),
1478 )));
1479 }
1480 let first = parsed.ast.body.remove(0);
1481 let ast::BodyItem::ExpressionStatement(expr_stmt) = first else {
1482 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1483 "Value expression must be a simple expression".to_string(),
1484 )));
1485 };
1486
1487 let new_value: ast::BinaryPart = expr_stmt
1488 .inner
1489 .expression
1490 .try_into()
1491 .map_err(|e: String| KclErrorWithOutputs::no_outputs(KclError::refactor(e)))?;
1492
1493 self.mutate_ast(
1494 &mut new_ast,
1495 constraint_id,
1496 AstMutateCommand::EditConstraintValue { value: new_value },
1497 )
1498 .map_err(KclErrorWithOutputs::no_outputs)?;
1499
1500 self.execute_after_edit(
1501 ctx,
1502 sketch,
1503 sketch_block_ref,
1504 &mut new_ast,
1505 ExecuteAfterEditOptions {
1506 segment_ids_edited: Default::default(),
1507 edit_kind: EditDeleteKind::Edit,
1508 commit_solved_initial_guesses: true,
1509 },
1510 )
1511 .await
1512 }
1513
1514 async fn edit_distance_constraint_label_position(
1515 &mut self,
1516 ctx: &ExecutorContext,
1517 _version: Version,
1518 sketch: ObjectId,
1519 constraint_id: ObjectId,
1520 label_position: Point2d<Number>,
1521 anchor_segment_ids: Vec<ObjectId>,
1522 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1523 let sketch_block_ref =
1525 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1526
1527 let object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
1528 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Object not found: {constraint_id:?}")))
1529 })?;
1530 if !matches!(
1531 &object.kind,
1532 ObjectKind::Constraint {
1533 constraint: Constraint::Distance(_)
1534 | Constraint::HorizontalDistance(_)
1535 | Constraint::VerticalDistance(_)
1536 | Constraint::Radius(_)
1537 | Constraint::Diameter(_),
1538 }
1539 ) {
1540 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1541 "Object does not support labelPosition: {constraint_id:?}"
1542 ))));
1543 }
1544
1545 let label_position = to_ast_point2d_number(&label_position).map_err(|err| {
1546 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1547 "Could not convert label position to AST: {err}"
1548 )))
1549 })?;
1550 let mut new_ast = self.program.ast.clone();
1551 self.mutate_ast(
1552 &mut new_ast,
1553 constraint_id,
1554 AstMutateCommand::EditDistanceConstraintLabelPosition { label_position },
1555 )
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 let mut new_objects = std::mem::take(&mut outcome.scene_objects);
4853
4854 if freedom_analysis_ran {
4855 self.point_freedom_cache.clear();
4858 for new_obj in &new_objects {
4859 if let ObjectKind::Segment {
4860 segment: crate::front::Segment::Point(point),
4861 } = &new_obj.kind
4862 {
4863 self.point_freedom_cache.insert(new_obj.id, point.freedom);
4864 }
4865 }
4866 add_wall_and_cap_face_objects(&mut new_objects, &outcome.artifact_graph);
4867 self.scene_graph.objects = new_objects;
4869 } else {
4870 for old_obj in &self.scene_graph.objects {
4873 if let ObjectKind::Segment {
4874 segment: crate::front::Segment::Point(point),
4875 } = &old_obj.kind
4876 {
4877 self.point_freedom_cache.insert(old_obj.id, point.freedom);
4878 }
4879 }
4880
4881 let mut updated_objects = Vec::with_capacity(new_objects.len());
4883 for new_obj in new_objects {
4884 let mut obj = new_obj;
4885 if let ObjectKind::Segment {
4886 segment: crate::front::Segment::Point(point),
4887 } = &mut obj.kind
4888 {
4889 let new_freedom = point.freedom;
4890 match new_freedom {
4896 Freedom::Free => {
4897 match self.point_freedom_cache.get(&obj.id).copied() {
4898 Some(Freedom::Conflict) => {
4899 }
4902 Some(Freedom::Fixed) => {
4903 point.freedom = Freedom::Fixed;
4905 }
4906 Some(Freedom::Free) => {
4907 }
4909 None => {
4910 }
4912 }
4913 }
4914 Freedom::Fixed => {
4915 }
4917 Freedom::Conflict => {
4918 }
4920 }
4921 self.point_freedom_cache.insert(obj.id, point.freedom);
4923 }
4924 updated_objects.push(obj);
4925 }
4926
4927 add_wall_and_cap_face_objects(&mut updated_objects, &outcome.artifact_graph);
4928 self.scene_graph.objects = updated_objects;
4929 }
4930 outcome
4931 }
4932
4933 fn mutate_ast(
4934 &mut self,
4935 ast: &mut ast::Node<ast::Program>,
4936 object_id: ObjectId,
4937 command: AstMutateCommand,
4938 ) -> Result<(AstNodeRef, AstMutateCommandReturn), KclError> {
4939 let sketch_object = self
4940 .scene_graph
4941 .objects
4942 .get(object_id.0)
4943 .ok_or_else(|| KclError::refactor(format!("Object not found: {object_id:?}")))?;
4944 mutate_ast_node_by_source_ref(ast, &sketch_object.source, command)
4945 }
4946}
4947
4948fn sketch_block_ref_from_id(scene_graph: &SceneGraph, sketch_id: ObjectId) -> Result<AstNodeRef, KclError> {
4949 let sketch_object = scene_graph
4951 .objects
4952 .get(sketch_id.0)
4953 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch_id:?}")))?;
4954 let ObjectKind::Sketch(_) = &sketch_object.kind else {
4955 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
4956 };
4957 expect_single_node_ref(sketch_object)
4958}
4959
4960fn expect_single_node_ref(object: &Object) -> Result<AstNodeRef, KclError> {
4961 match &object.source {
4962 SourceRef::Simple { range, node_path } => Ok(AstNodeRef {
4963 range: *range,
4964 node_path: node_path.clone(),
4965 }),
4966 SourceRef::BackTrace { ranges } => {
4967 let [range] = ranges.as_slice() else {
4968 return Err(KclError::refactor(format!(
4969 "Expected single location in SourceRef, got {}; ranges={ranges:#?}",
4970 ranges.len()
4971 )));
4972 };
4973 Ok(AstNodeRef {
4974 range: range.0,
4975 node_path: range.1.clone(),
4976 })
4977 }
4978 }
4979}
4980
4981fn only_sketch_block_from_range(
4984 ast: &mut ast::Node<ast::Program>,
4985 sketch_block_range: SourceRange,
4986 edit_kind: ChangeKind,
4987) -> Result<(), KclError> {
4988 let r1 = sketch_block_range;
4989 let matches_range = |r2: SourceRange| -> bool {
4990 match edit_kind {
4993 ChangeKind::Add => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() <= r2.end(),
4994 ChangeKind::Edit => r1.module_id() == r2.module_id() && r1.start() == r2.start(),
4996 ChangeKind::Delete => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() >= r2.end(),
4997 ChangeKind::None => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() == r2.end(),
4999 }
5000 };
5001 let mut found = false;
5002 for item in ast.body.iter_mut() {
5003 match item {
5004 ast::BodyItem::ImportStatement(_) => {}
5005 ast::BodyItem::ExpressionStatement(node) => {
5006 if matches_range(SourceRange::from(&*node))
5007 && let ast::Expr::SketchBlock(sketch_block) = &mut node.expression
5008 {
5009 sketch_block.is_being_edited = true;
5010 found = true;
5011 break;
5012 }
5013 }
5014 ast::BodyItem::VariableDeclaration(node) => {
5015 if matches_range(SourceRange::from(&node.declaration.init))
5016 && let ast::Expr::SketchBlock(sketch_block) = &mut node.declaration.init
5017 {
5018 sketch_block.is_being_edited = true;
5019 found = true;
5020 break;
5021 }
5022 }
5023 ast::BodyItem::TypeDeclaration(_) => {}
5024 ast::BodyItem::ReturnStatement(node) => {
5025 if matches_range(SourceRange::from(&node.argument))
5026 && let ast::Expr::SketchBlock(sketch_block) = &mut node.argument
5027 {
5028 sketch_block.is_being_edited = true;
5029 found = true;
5030 break;
5031 }
5032 }
5033 }
5034 }
5035 if !found {
5036 return Err(KclError::refactor(format!(
5037 "Sketch block source range not found in AST: {sketch_block_range:?}, edit_kind={edit_kind:?}"
5038 )));
5039 }
5040
5041 Ok(())
5042}
5043
5044fn only_sketch_block(
5045 ast: &mut ast::Node<ast::Program>,
5046 sketch_block_ref: &AstNodeRef,
5047 edit_kind: ChangeKind,
5048) -> Result<(), KclError> {
5049 let Some(target_node_path) = &sketch_block_ref.node_path else {
5050 #[cfg(target_arch = "wasm32")]
5051 web_sys::console::warn_1(
5052 &format!(
5053 "only_sketch_block: target sketch block ref doesn't have node path; sketch_block_ref={:#?}, edit_kind={edit_kind:#?}",
5054 sketch_block_ref
5055 )
5056 .into(),
5057 );
5058 return only_sketch_block_from_range(ast, sketch_block_ref.range, edit_kind);
5059 };
5060 let mut found = false;
5061 for item in ast.body.iter_mut() {
5062 match item {
5063 ast::BodyItem::ImportStatement(_) => {}
5064 ast::BodyItem::ExpressionStatement(node) => {
5065 if let Some(node_path) = &node.node_path
5067 && node_path == target_node_path
5068 && let ast::Expr::SketchBlock(sketch_block) = &mut node.expression
5069 {
5070 sketch_block.is_being_edited = true;
5071 found = true;
5072 break;
5073 }
5074 if let Some(node_path) = node.expression.node_path()
5076 && node_path == target_node_path
5077 && let ast::Expr::SketchBlock(sketch_block) = &mut node.expression
5078 {
5079 sketch_block.is_being_edited = true;
5080 found = true;
5081 break;
5082 }
5083 }
5084 ast::BodyItem::VariableDeclaration(node) => {
5085 if let Some(node_path) = node.declaration.init.node_path()
5086 && node_path == target_node_path
5087 && let ast::Expr::SketchBlock(sketch_block) = &mut node.declaration.init
5088 {
5089 sketch_block.is_being_edited = true;
5090 found = true;
5091 break;
5092 }
5093 }
5094 ast::BodyItem::TypeDeclaration(_) => {}
5095 ast::BodyItem::ReturnStatement(node) => {
5096 if let Some(node_path) = node.argument.node_path()
5097 && node_path == target_node_path
5098 && let ast::Expr::SketchBlock(sketch_block) = &mut node.argument
5099 {
5100 sketch_block.is_being_edited = true;
5101 found = true;
5102 break;
5103 }
5104 }
5105 }
5106 }
5107 if !found {
5108 return Err(KclError::refactor(format!(
5109 "Sketch block node path not found in AST: {sketch_block_ref:?}, edit_kind={edit_kind:?}"
5110 )));
5111 }
5112
5113 Ok(())
5114}
5115
5116fn sketch_on_ast_expr(
5117 ast: &mut ast::Node<ast::Program>,
5118 scene_graph: &SceneGraph,
5119 on: &Plane,
5120) -> Result<ast::Expr, KclError> {
5121 match on {
5122 Plane::Default(name) => Ok(default_plane_ast_expr(*name)),
5123 Plane::Object(object_id) => {
5124 let on_object = scene_graph
5125 .objects
5126 .get(object_id.0)
5127 .ok_or_else(|| KclError::refactor(format!("Sketch plane object not found: {object_id:?}")))?;
5128 if let Some(face_expr) = sketch_face_of_scene_object_ast_expr(ast, on_object)? {
5129 return Ok(face_expr);
5130 }
5131 get_or_insert_ast_reference(ast, &on_object.source, "plane", None)
5132 }
5133 }
5134}
5135
5136fn sketch_face_of_scene_object_ast_expr(
5137 ast: &mut ast::Node<ast::Program>,
5138 on_object: &crate::front::Object,
5139) -> Result<Option<ast::Expr>, KclError> {
5140 match &on_object.kind {
5141 ObjectKind::Wall(wall) => {
5142 let solid_ref = get_or_insert_ast_reference(
5143 ast,
5144 &source_ref_from_source_ref_range(&wall.source.solid),
5145 "solid",
5146 None,
5147 )?;
5148 let ast::Expr::Name(solid_name_expr) = solid_ref else {
5149 return Err(KclError::refactor(format!(
5150 "Could not resolve solid reference for selected wall: artifact_id={:?}",
5151 on_object.artifact_id
5152 )));
5153 };
5154 let solid_expr = indexed_solid_expr_for_sweep_output(
5155 ast_name_expr(solid_name_expr.name.name.clone()),
5156 wall.solid_output_index,
5157 );
5158 let sweep_ref = get_or_insert_ast_reference(
5159 ast,
5160 &source_ref_from_source_ref_range(&wall.source.sweep),
5161 "solid",
5162 None,
5163 )?;
5164 let ast::Expr::Name(sweep_name_expr) = sweep_ref else {
5165 return Err(KclError::refactor(format!(
5166 "Could not resolve sweep reference for selected wall: artifact_id={:?}",
5167 on_object.artifact_id
5168 )));
5169 };
5170 let sweep_name = sweep_name_expr.name.name.clone();
5171 let segment_ref = get_or_insert_ast_reference(
5172 ast,
5173 &source_ref_from_source_ref_range(&wall.source.segment),
5174 LINE_VARIABLE,
5175 None,
5176 )?;
5177
5178 let face_expr = if let Some(region_name) = region_name_from_sweep_variable(ast, &sweep_name).or_else(|| {
5179 wall.source
5180 .path
5181 .as_ref()
5182 .and_then(|path_source| region_name_from_path_source(ast, path_source))
5183 }) {
5184 let ast::Expr::Name(segment_name_expr) = segment_ref else {
5185 return Err(KclError::refactor(format!(
5186 "Could not resolve source segment reference for selected region wall: artifact_id={:?}",
5187 on_object.artifact_id
5188 )));
5189 };
5190 create_member_expression(
5191 create_member_expression(ast_name_expr(region_name), "tags"),
5192 &segment_name_expr.name.name,
5193 )
5194 } else {
5195 segment_ref
5196 };
5197
5198 Ok(Some(create_face_of_ast(solid_expr, face_expr)))
5199 }
5200 ObjectKind::Cap(cap) => {
5201 let solid_ref =
5202 get_or_insert_ast_reference(ast, &source_ref_from_source_ref_range(&cap.source.solid), "solid", None)?;
5203 let ast::Expr::Name(solid_name_expr) = solid_ref else {
5204 return Err(KclError::refactor(format!(
5205 "Could not resolve solid reference for selected cap: artifact_id={:?}",
5206 on_object.artifact_id
5207 )));
5208 };
5209 let solid_expr = indexed_solid_expr_for_sweep_output(
5210 ast_name_expr(solid_name_expr.name.name.clone()),
5211 cap.solid_output_index,
5212 );
5213 let face_expr = match cap.kind {
5215 crate::frontend::api::CapKind::Start => ast_name_expr("START".to_owned()),
5216 crate::frontend::api::CapKind::End => ast_name_expr("END".to_owned()),
5217 };
5218
5219 Ok(Some(create_face_of_ast(solid_expr, face_expr)))
5220 }
5221 _ => Ok(None),
5222 }
5223}
5224
5225fn indexed_solid_expr_for_sweep_output(solid_expr: ast::Expr, solid_output_index: Option<usize>) -> ast::Expr {
5226 match solid_output_index {
5227 Some(output_index) => create_index_expression(solid_expr, output_index),
5228 None => solid_expr,
5229 }
5230}
5231
5232fn source_ref_from_source_ref_range(source: &SourceRefRange) -> SourceRef {
5233 SourceRef::Simple {
5234 range: source.range,
5235 node_path: source.node_path.clone(),
5236 }
5237}
5238
5239fn region_name_from_path_source(ast: &ast::Node<ast::Program>, path_source: &SourceRefRange) -> Option<String> {
5240 let source_ref = source_ref_from_source_ref_range(path_source);
5241 let candidate = variable_name_containing_source_ref(ast, &source_ref)?;
5242 let ast::Definition::Variable(region_decl) = ast.get_variable(&candidate)? else {
5243 return None;
5244 };
5245 let ast::Expr::CallExpressionKw(region_call) = ®ion_decl.init else {
5246 return None;
5247 };
5248 if region_call.callee.name.name != "region" {
5249 return None;
5250 }
5251 Some(candidate)
5252}
5253
5254fn downstream_composite_code_ref_for_source(artifact_graph: &ArtifactGraph, source_id: ArtifactId) -> Option<&CodeRef> {
5255 let mut current_id = source_id;
5256 let mut current_composite = None;
5257 let mut visited = HashSet::new();
5258
5259 while visited.insert(current_id) {
5260 let next_composite_id = downstream_composite_id_for_solid_source(artifact_graph, current_id);
5261
5262 let Some(composite_id) = next_composite_id else {
5263 break;
5264 };
5265 let Some(Artifact::CompositeSolid(composite)) = artifact_graph.get(&composite_id) else {
5266 break;
5267 };
5268
5269 current_id = composite.id;
5270 current_composite = Some(composite);
5271
5272 if !composite.consumed {
5273 break;
5274 }
5275 }
5276
5277 current_composite.map(|composite| &composite.code_ref)
5278}
5279
5280fn downstream_composite_id_for_solid_source(
5281 artifact_graph: &ArtifactGraph,
5282 source_id: ArtifactId,
5283) -> Option<ArtifactId> {
5284 if let Some(Artifact::Path(path)) = artifact_graph.get(&source_id)
5286 && let Some(composite_id) = path.composite_solid_id
5287 && let Some(Artifact::CompositeSolid(composite)) = artifact_graph.get(&composite_id)
5288 && composite_contains_path_input(&composite.solid_ids, &composite.tool_ids, path.id, path.solid2d_id)
5289 {
5290 return Some(composite_id);
5291 }
5292
5293 for artifact in artifact_graph.values() {
5295 if let Artifact::Path(path) = artifact
5296 && path.sweep_id == Some(source_id)
5297 && let Some(composite_id) = path.composite_solid_id
5298 && let Some(Artifact::CompositeSolid(composite)) = artifact_graph.get(&composite_id)
5299 && composite_contains_path_input(&composite.solid_ids, &composite.tool_ids, path.id, path.solid2d_id)
5300 {
5301 return Some(composite_id);
5302 }
5303 }
5304
5305 artifact_graph.values().find_map(|artifact| {
5307 let Artifact::CompositeSolid(composite) = artifact else {
5308 return None;
5309 };
5310 composite_contains_input(&composite.solid_ids, &composite.tool_ids, source_id).then_some(composite.id)
5311 })
5312}
5313
5314fn composite_contains_path_input(
5315 solid_ids: &[ArtifactId],
5316 tool_ids: &[ArtifactId],
5317 path_id: ArtifactId,
5318 solid2d_id: Option<ArtifactId>,
5319) -> bool {
5320 composite_contains_input(solid_ids, tool_ids, path_id)
5321 || solid2d_id.is_some_and(|solid2d_id| composite_contains_input(solid_ids, tool_ids, solid2d_id))
5322}
5323
5324fn composite_contains_input(solid_ids: &[ArtifactId], tool_ids: &[ArtifactId], input_id: ArtifactId) -> bool {
5325 solid_ids.contains(&input_id) || tool_ids.contains(&input_id)
5326}
5327
5328fn code_ref_source_ref_range(code_ref: &CodeRef) -> SourceRefRange {
5329 let node_path = (!code_ref.node_path.is_empty()).then(|| code_ref.node_path.clone());
5330 SourceRefRange {
5331 range: code_ref.range,
5332 node_path,
5333 }
5334}
5335
5336fn solid_output_index_for_sweep(
5337 artifact_graph: &ArtifactGraph,
5338 sweep_id: ArtifactId,
5339 sweep_code_ref: &CodeRef,
5340) -> Option<usize> {
5341 let sibling_sweeps = artifact_graph
5342 .values()
5343 .filter_map(|artifact| match artifact {
5344 Artifact::Sweep(sweep)
5345 if sweep.code_ref.range == sweep_code_ref.range
5346 && sweep.code_ref.node_path == sweep_code_ref.node_path =>
5347 {
5348 Some(sweep)
5349 }
5350 _ => None,
5351 })
5352 .collect::<Vec<_>>();
5353
5354 if sibling_sweeps.len() <= 1 {
5355 return None;
5356 }
5357
5358 sibling_sweeps
5359 .iter()
5360 .position(|sibling_sweep| sibling_sweep.id == sweep_id)
5361}
5362
5363fn add_wall_and_cap_face_objects(scene_objects: &mut Vec<crate::front::Object>, artifact_graph: &ArtifactGraph) {
5364 let mut existing_artifact_ids = scene_objects
5365 .iter()
5366 .map(|object| object.artifact_id)
5367 .collect::<HashSet<_>>();
5368
5369 for artifact in artifact_graph.values() {
5370 match artifact {
5371 Artifact::Wall(wall) => {
5372 if existing_artifact_ids.contains(&wall.id) {
5373 continue;
5374 }
5375
5376 let Some(segment) = artifact_graph.get(&wall.seg_id).and_then(|artifact| match artifact {
5377 Artifact::Segment(segment) => Some(segment),
5378 _ => None,
5379 }) else {
5380 continue;
5381 };
5382 let Some(sweep) = artifact_graph.get(&wall.sweep_id).and_then(|artifact| match artifact {
5383 Artifact::Sweep(sweep) => Some(sweep),
5384 _ => None,
5385 }) else {
5386 continue;
5387 };
5388 let source_segment = segment
5389 .original_seg_id
5390 .and_then(|original_seg_id| artifact_graph.get(&original_seg_id))
5391 .and_then(|artifact| match artifact {
5392 Artifact::Segment(segment) => Some(segment),
5393 _ => None,
5394 })
5395 .unwrap_or(segment);
5396 let solid_code_ref =
5397 downstream_composite_code_ref_for_source(artifact_graph, wall.sweep_id).unwrap_or(&sweep.code_ref);
5398 let path_code_ref = artifact_graph
5399 .get(&segment.path_id)
5400 .or_else(|| artifact_graph.get(&sweep.path_id))
5401 .and_then(|artifact| match artifact {
5402 Artifact::Path(path) => Some(&path.code_ref),
5403 _ => None,
5404 });
5405 let source = WallSource {
5406 solid: code_ref_source_ref_range(solid_code_ref),
5407 sweep: code_ref_source_ref_range(&sweep.code_ref),
5408 path: path_code_ref.map(code_ref_source_ref_range),
5409 segment: code_ref_source_ref_range(&source_segment.code_ref),
5410 };
5411 let solid_output_index = (solid_code_ref.range == sweep.code_ref.range
5412 && solid_code_ref.node_path == sweep.code_ref.node_path)
5413 .then(|| solid_output_index_for_sweep(artifact_graph, sweep.id, &sweep.code_ref))
5414 .flatten();
5415 let object_source = source_ref_from_source_ref_range(&source.solid);
5416 let id = ObjectId(scene_objects.len());
5417 scene_objects.push(crate::front::Object {
5418 id,
5419 kind: ObjectKind::Wall(crate::frontend::api::Wall {
5420 id,
5421 source,
5422 solid_output_index,
5423 }),
5424 label: Default::default(),
5425 comments: Default::default(),
5426 artifact_id: wall.id,
5427 source: object_source,
5428 });
5429 existing_artifact_ids.insert(wall.id);
5430 }
5431 Artifact::Cap(cap) => {
5432 if existing_artifact_ids.contains(&cap.id) {
5433 continue;
5434 }
5435
5436 let Some(sweep) = artifact_graph.get(&cap.sweep_id).and_then(|artifact| match artifact {
5437 Artifact::Sweep(sweep) => Some(sweep),
5438 _ => None,
5439 }) else {
5440 continue;
5441 };
5442 let id = ObjectId(scene_objects.len());
5443 let kind = match cap.sub_type {
5444 CapSubType::Start => crate::frontend::api::CapKind::Start,
5445 CapSubType::End => crate::frontend::api::CapKind::End,
5446 };
5447 let solid_code_ref =
5448 downstream_composite_code_ref_for_source(artifact_graph, cap.sweep_id).unwrap_or(&sweep.code_ref);
5449 let source = CapSource {
5450 solid: code_ref_source_ref_range(solid_code_ref),
5451 sweep: code_ref_source_ref_range(&sweep.code_ref),
5452 };
5453 let solid_output_index = (solid_code_ref.range == sweep.code_ref.range
5454 && solid_code_ref.node_path == sweep.code_ref.node_path)
5455 .then(|| solid_output_index_for_sweep(artifact_graph, sweep.id, &sweep.code_ref))
5456 .flatten();
5457 let object_source = source_ref_from_source_ref_range(&source.solid);
5458 scene_objects.push(crate::front::Object {
5459 id,
5460 kind: ObjectKind::Cap(crate::frontend::api::Cap {
5461 id,
5462 kind,
5463 source,
5464 solid_output_index,
5465 }),
5466 label: Default::default(),
5467 comments: Default::default(),
5468 artifact_id: cap.id,
5469 source: object_source,
5470 });
5471 existing_artifact_ids.insert(cap.id);
5472 }
5473 _ => {}
5474 }
5475 }
5476}
5477
5478fn default_plane_ast_expr(name: crate::engine::PlaneName) -> ast::Expr {
5479 use crate::engine::PlaneName;
5480
5481 match name {
5482 PlaneName::Xy => ast_name_expr("XY".to_owned()),
5483 PlaneName::Xz => ast_name_expr("XZ".to_owned()),
5484 PlaneName::Yz => ast_name_expr("YZ".to_owned()),
5485 PlaneName::NegXy => negated_plane_ast_expr("XY"),
5486 PlaneName::NegXz => negated_plane_ast_expr("XZ"),
5487 PlaneName::NegYz => negated_plane_ast_expr("YZ"),
5488 }
5489}
5490
5491fn negated_plane_ast_expr(name: &str) -> ast::Expr {
5492 ast::Expr::UnaryExpression(Box::new(ast::UnaryExpression::new(
5493 ast::UnaryOperator::Neg,
5494 ast::BinaryPart::Name(Box::new(ast_name(name.to_owned()))),
5495 )))
5496}
5497
5498fn create_face_of_ast(solid_expr: ast::Expr, face_expr: ast::Expr) -> ast::Expr {
5499 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5500 callee: ast::Node::no_src(ast_sketch2_name("faceOf")),
5501 unlabeled: Some(solid_expr),
5502 arguments: vec![ast::LabeledArg {
5503 label: Some(ast::Identifier::new("face")),
5504 arg: face_expr,
5505 }],
5506 digest: None,
5507 non_code_meta: Default::default(),
5508 })))
5509}
5510
5511fn region_name_from_sweep_variable(ast: &ast::Node<ast::Program>, sweep_variable_name: &str) -> Option<String> {
5512 let ast::Definition::Variable(sweep_decl) = ast.get_variable(sweep_variable_name)? else {
5513 return None;
5514 };
5515 let ast::Expr::CallExpressionKw(sweep_call) = &sweep_decl.init else {
5516 return None;
5517 };
5518 if !matches!(
5519 sweep_call.callee.name.name.as_str(),
5520 "extrude" | "revolve" | "sweep" | "loft"
5521 ) {
5522 return None;
5523 }
5524 let ast::Expr::Name(region_name_expr) = sweep_call.unlabeled.as_ref()? else {
5525 return None;
5526 };
5527 let candidate = region_name_expr.name.name.clone();
5528 let ast::Definition::Variable(region_decl) = ast.get_variable(&candidate)? else {
5529 return None;
5530 };
5531 let ast::Expr::CallExpressionKw(region_call) = ®ion_decl.init else {
5532 return None;
5533 };
5534 if region_call.callee.name.name != "region" {
5535 return None;
5536 }
5537 Some(candidate)
5538}
5539
5540fn get_or_insert_ast_reference(
5547 ast: &mut ast::Node<ast::Program>,
5548 source_ref: &SourceRef,
5549 prefix: &str,
5550 property: Option<&str>,
5551) -> Result<ast::Expr, KclError> {
5552 let command = AstMutateCommand::AddVariableDeclaration {
5553 prefix: prefix.to_owned(),
5554 };
5555 let ret = match mutate_ast_node_by_source_ref(ast, source_ref, command) {
5556 Ok((_, ret)) => ret,
5557 Err(err) => {
5558 if let Some(var_name) = variable_name_containing_source_ref(ast, source_ref) {
5559 AstMutateCommandReturn::Name(var_name)
5560 } else {
5561 return Err(err);
5562 }
5563 }
5564 };
5565 let AstMutateCommandReturn::Name(var_name) = ret else {
5566 return Err(KclError::refactor(
5567 "Expected variable name returned from AddVariableDeclaration".to_owned(),
5568 ));
5569 };
5570 let var_expr = ast::Expr::Name(Box::new(ast::Name::new(&var_name)));
5571 let Some(property) = property else {
5572 return Ok(var_expr);
5574 };
5575
5576 Ok(create_member_expression(var_expr, property))
5577}
5578
5579fn variable_name_containing_source_ref(ast: &ast::Node<ast::Program>, source_ref: &SourceRef) -> Option<String> {
5580 let source_range = match source_ref {
5581 SourceRef::Simple { range, .. } => *range,
5582 SourceRef::BackTrace { ranges } => {
5583 let [range] = ranges.as_slice() else {
5584 return None;
5585 };
5586 range.0
5587 }
5588 };
5589 ast.body.iter().find_map(|item| {
5590 let ast::BodyItem::VariableDeclaration(var_decl) = item else {
5591 return None;
5592 };
5593 let init_range = SourceRange::from(&var_decl.declaration.init);
5594 let source_is_inside_init = init_range.module_id() == source_range.module_id()
5595 && init_range.start() <= source_range.start()
5596 && source_range.end() <= init_range.end();
5597 if matches!(&var_decl.declaration.init, ast::Expr::SketchBlock(_))
5598 && init_range != source_range
5599 && source_is_inside_init
5600 {
5601 return None;
5602 }
5603 source_is_inside_init.then(|| var_decl.name().to_owned())
5604 })
5605}
5606
5607fn mutate_ast_node_by_source_ref(
5608 ast: &mut ast::Node<ast::Program>,
5609 source_ref: &SourceRef,
5610 command: AstMutateCommand,
5611) -> Result<(AstNodeRef, AstMutateCommandReturn), KclError> {
5612 let (source_range, node_path) = match source_ref {
5613 SourceRef::Simple { range, node_path } => (*range, node_path.clone()),
5614 SourceRef::BackTrace { ranges } => {
5615 let [range] = ranges.as_slice() else {
5616 return Err(KclError::refactor(format!(
5617 "Expected single source ref, got {}; ranges={ranges:#?}",
5618 ranges.len(),
5619 )));
5620 };
5621 (range.0, range.1.clone())
5622 }
5623 };
5624 let mut context = AstMutateContext {
5625 source_range,
5626 node_path,
5627 command,
5628 defined_names_stack: Default::default(),
5629 };
5630 let control = dfs_mut(ast, &mut context);
5631 match control {
5632 ControlFlow::Continue(_) => Err(KclError::refactor(
5633 "Could not find the KCL source for this edit. Try reloading the app, or update from code.".to_owned(),
5634 )),
5635 ControlFlow::Break(break_value) => break_value,
5636 }
5637}
5638
5639#[derive(Debug)]
5640struct AstMutateContext {
5641 source_range: SourceRange,
5642 node_path: Option<ast::NodePath>,
5643 command: AstMutateCommand,
5644 defined_names_stack: Vec<HashSet<String>>,
5645}
5646
5647#[derive(Debug)]
5648#[allow(clippy::large_enum_variant)]
5649enum AstMutateCommand {
5650 AddSketchBlockExprStmt {
5652 expr: ast::Expr,
5653 },
5654 AddSketchBlockVarDecl {
5656 prefix: String,
5657 expr: ast::Expr,
5658 },
5659 AddVariableDeclaration {
5660 prefix: String,
5661 },
5662 EditPoint {
5663 at: ast::Expr,
5664 },
5665 EditLine {
5666 start: ast::Expr,
5667 end: ast::Expr,
5668 construction: Option<bool>,
5669 },
5670 EditArc {
5671 start: ast::Expr,
5672 end: ast::Expr,
5673 center: ast::Expr,
5674 construction: Option<bool>,
5675 },
5676 EditCircle {
5677 start: ast::Expr,
5678 center: ast::Expr,
5679 construction: Option<bool>,
5680 },
5681 EditControlPointSpline {
5682 points: ast::Expr,
5683 construction: Option<bool>,
5684 },
5685 EditConstraintValue {
5686 value: ast::BinaryPart,
5687 },
5688 EditDistanceConstraintLabelPosition {
5689 label_position: ast::Expr,
5690 },
5691 EditCallUnlabeled {
5692 arg: ast::Expr,
5693 },
5694 EditVarInitialValue {
5695 value: Number,
5696 },
5697 DeleteNode,
5698}
5699
5700impl AstMutateCommand {
5701 fn needs_defined_names_stack(&self) -> bool {
5702 matches!(
5703 self,
5704 AstMutateCommand::AddSketchBlockVarDecl { .. } | AstMutateCommand::AddVariableDeclaration { .. }
5705 )
5706 }
5707}
5708
5709#[derive(Debug)]
5710enum AstMutateCommandReturn {
5711 None,
5712 Name(String),
5713}
5714
5715#[derive(Debug, Clone)]
5716struct AstNodeRef {
5717 range: SourceRange,
5718 node_path: Option<ast::NodePath>,
5719}
5720
5721impl<T> From<&ast::Node<T>> for AstNodeRef {
5722 fn from(value: &ast::Node<T>) -> Self {
5723 AstNodeRef {
5724 range: value.into(),
5725 node_path: value.node_path.clone(),
5726 }
5727 }
5728}
5729
5730impl From<&ast::BodyItem> for AstNodeRef {
5731 fn from(value: &ast::BodyItem) -> Self {
5732 match value {
5733 ast::BodyItem::ImportStatement(node) => AstNodeRef {
5734 range: node.into(),
5735 node_path: node.node_path.clone(),
5736 },
5737 ast::BodyItem::ExpressionStatement(node) => AstNodeRef {
5738 range: node.into(),
5739 node_path: node.node_path.clone(),
5740 },
5741 ast::BodyItem::VariableDeclaration(node) => AstNodeRef {
5742 range: node.into(),
5743 node_path: node.node_path.clone(),
5744 },
5745 ast::BodyItem::TypeDeclaration(node) => AstNodeRef {
5746 range: node.into(),
5747 node_path: node.node_path.clone(),
5748 },
5749 ast::BodyItem::ReturnStatement(node) => AstNodeRef {
5750 range: node.into(),
5751 node_path: node.node_path.clone(),
5752 },
5753 }
5754 }
5755}
5756
5757impl From<&ast::Expr> for AstNodeRef {
5758 fn from(value: &ast::Expr) -> Self {
5759 AstNodeRef {
5760 range: SourceRange::from(value),
5761 node_path: value.node_path().cloned(),
5762 }
5763 }
5764}
5765
5766impl From<&AstMutateContext> for AstNodeRef {
5767 fn from(value: &AstMutateContext) -> Self {
5768 AstNodeRef {
5769 range: value.source_range,
5770 node_path: value.node_path.clone(),
5771 }
5772 }
5773}
5774
5775impl TryFrom<&NodeMut<'_>> for AstNodeRef {
5776 type Error = crate::walk::AstNodeError;
5777
5778 fn try_from(value: &NodeMut<'_>) -> Result<Self, Self::Error> {
5779 Ok(AstNodeRef {
5780 range: SourceRange::try_from(value)?,
5781 node_path: value.try_into()?,
5782 })
5783 }
5784}
5785
5786impl From<AstNodeRef> for SourceRange {
5787 fn from(value: AstNodeRef) -> Self {
5788 value.range
5789 }
5790}
5791
5792impl Visitor for AstMutateContext {
5793 type Break = Result<(AstNodeRef, AstMutateCommandReturn), KclError>;
5794 type Continue = ();
5795
5796 fn visit(&mut self, node: NodeMut<'_>) -> TraversalReturn<Self::Break, Self::Continue> {
5797 filter_and_process(self, node)
5798 }
5799
5800 fn finish(&mut self, node: NodeMut<'_>) {
5801 match &node {
5802 NodeMut::Program(_) | NodeMut::SketchBlock(_) => {
5803 self.defined_names_stack.pop();
5804 }
5805 _ => {}
5806 }
5807 }
5808}
5809
5810fn filter_and_process(
5811 ctx: &mut AstMutateContext,
5812 node: NodeMut,
5813) -> TraversalReturn<Result<(AstNodeRef, AstMutateCommandReturn), KclError>> {
5814 let Ok(node_range) = SourceRange::try_from(&node) else {
5815 return TraversalReturn::new_continue(());
5817 };
5818 if let NodeMut::VariableDeclaration(var_decl) = &node {
5823 let expr_range = SourceRange::from(&var_decl.declaration.init);
5824 let expr_node_path = var_decl.declaration.init.node_path();
5825 if source_ref_matches(ctx, expr_range, expr_node_path) {
5826 if let AstMutateCommand::AddVariableDeclaration { .. } = &ctx.command {
5827 return TraversalReturn::new_break(Ok((
5830 AstNodeRef::from(&**var_decl),
5831 AstMutateCommandReturn::Name(var_decl.name().to_owned()),
5832 )));
5833 }
5834 if let AstMutateCommand::DeleteNode = &ctx.command {
5835 return TraversalReturn {
5838 mutate_body_item: MutateBodyItem::Delete,
5839 control_flow: ControlFlow::Break(Ok((AstNodeRef::from(&*ctx), AstMutateCommandReturn::None))),
5840 };
5841 }
5842 }
5843 }
5844 if let NodeMut::ExpressionStatement(expr_stmt) = &node {
5847 let expr_range = SourceRange::from(&expr_stmt.expression);
5848 let expr_node_path = expr_stmt.expression.node_path();
5849 if source_ref_matches(ctx, expr_range, expr_node_path) {
5850 if let AstMutateCommand::AddVariableDeclaration { .. } = &ctx.command {
5851 let Ok(node_ref) = AstNodeRef::try_from(&node) else {
5854 return TraversalReturn::new_continue(());
5855 };
5856 return process(ctx, node).map_break(|result| result.map(|cmd_return| (node_ref, cmd_return)));
5857 }
5858 if let AstMutateCommand::DeleteNode = &ctx.command {
5859 return TraversalReturn {
5862 mutate_body_item: MutateBodyItem::Delete,
5863 control_flow: ControlFlow::Break(Ok((AstNodeRef::from(&*ctx), AstMutateCommandReturn::None))),
5864 };
5865 }
5866 }
5867 }
5868
5869 if ctx.command.needs_defined_names_stack() {
5870 if let NodeMut::Program(program) = &node {
5871 ctx.defined_names_stack.push(find_defined_names(*program));
5872 } else if let NodeMut::SketchBlock(block) = &node {
5873 ctx.defined_names_stack.push(find_defined_names(&block.body));
5874 }
5875 }
5876
5877 let node_path = <Option<ast::NodePath>>::try_from(&node).ok().flatten();
5879 if !source_ref_matches(ctx, node_range, node_path.as_ref()) {
5880 return TraversalReturn::new_continue(());
5881 }
5882 let Ok(node_ref) = AstNodeRef::try_from(&node) else {
5883 return TraversalReturn::new_continue(());
5884 };
5885 process(ctx, node).map_break(|result| result.map(|cmd_return| (node_ref, cmd_return)))
5886}
5887
5888fn source_ref_matches(ctx: &AstMutateContext, node_range: SourceRange, node_path: Option<&ast::NodePath>) -> bool {
5889 match &ctx.node_path {
5890 Some(target) => Some(target) == node_path,
5891 None => node_range == ctx.source_range,
5892 }
5893}
5894
5895fn process(ctx: &AstMutateContext, node: NodeMut) -> TraversalReturn<Result<AstMutateCommandReturn, KclError>> {
5896 match &ctx.command {
5897 AstMutateCommand::AddSketchBlockExprStmt { expr } => {
5898 if let NodeMut::SketchBlock(sketch_block) = node {
5899 sketch_block
5900 .body
5901 .items
5902 .push(ast::BodyItem::ExpressionStatement(ast::Node {
5903 inner: ast::ExpressionStatement {
5904 expression: expr.clone(),
5905 digest: None,
5906 },
5907 start: Default::default(),
5908 end: Default::default(),
5909 module_id: Default::default(),
5910 node_path: None,
5911 outer_attrs: Default::default(),
5912 pre_comments: Default::default(),
5913 comment_start: Default::default(),
5914 }));
5915 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5916 }
5917 }
5918 AstMutateCommand::AddSketchBlockVarDecl { prefix, expr } => {
5919 if let NodeMut::SketchBlock(sketch_block) = node {
5920 let empty_defined_names = HashSet::new();
5921 let defined_names = ctx.defined_names_stack.last().unwrap_or(&empty_defined_names);
5922 let Ok(name) = next_free_name(prefix, defined_names) else {
5923 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5924 };
5925 sketch_block
5926 .body
5927 .items
5928 .push(ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(
5929 ast::VariableDeclaration::new(
5930 ast::VariableDeclarator::new(&name, expr.clone()),
5931 ast::ItemVisibility::Default,
5932 ast::VariableKind::Const,
5933 ),
5934 ))));
5935 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::Name(name)));
5936 }
5937 }
5938 AstMutateCommand::AddVariableDeclaration { prefix } => {
5939 if let NodeMut::VariableDeclaration(inner) = node {
5940 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::Name(inner.name().to_owned())));
5941 }
5942 if let NodeMut::ExpressionStatement(expr_stmt) = node {
5943 let empty_defined_names = HashSet::new();
5944 let defined_names = ctx.defined_names_stack.last().unwrap_or(&empty_defined_names);
5945 let Ok(name) = next_free_name(prefix, defined_names) else {
5946 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5948 };
5949 let mutate_node =
5950 ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(ast::VariableDeclaration::new(
5951 ast::VariableDeclarator::new(&name, expr_stmt.expression.clone()),
5952 ast::ItemVisibility::Default,
5953 ast::VariableKind::Const,
5954 ))));
5955 return TraversalReturn {
5956 mutate_body_item: MutateBodyItem::Mutate(Box::new(mutate_node)),
5957 control_flow: ControlFlow::Break(Ok(AstMutateCommandReturn::Name(name))),
5958 };
5959 }
5960 }
5961 AstMutateCommand::EditPoint { at } => {
5962 if let NodeMut::CallExpressionKw(call) = node {
5963 if call.callee.name.name != POINT_FN {
5964 return TraversalReturn::new_continue(());
5965 }
5966 for labeled_arg in &mut call.arguments {
5968 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(POINT_AT_PARAM) {
5969 labeled_arg.arg = at.clone();
5970 }
5971 }
5972 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5973 }
5974 }
5975 AstMutateCommand::EditLine {
5976 start,
5977 end,
5978 construction,
5979 } => {
5980 if let NodeMut::CallExpressionKw(call) = node {
5981 if call.callee.name.name != LINE_FN {
5982 return TraversalReturn::new_continue(());
5983 }
5984 for labeled_arg in &mut call.arguments {
5986 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(LINE_START_PARAM) {
5987 labeled_arg.arg = start.clone();
5988 }
5989 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(LINE_END_PARAM) {
5990 labeled_arg.arg = end.clone();
5991 }
5992 }
5993 if let Some(construction_value) = construction {
5995 let construction_exists = call
5996 .arguments
5997 .iter()
5998 .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
5999 if *construction_value {
6000 if construction_exists {
6002 for labeled_arg in &mut call.arguments {
6004 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
6005 labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
6006 value: ast::LiteralValue::Bool(true),
6007 raw: "true".to_string(),
6008 digest: None,
6009 })));
6010 }
6011 }
6012 } else {
6013 call.arguments.push(ast::LabeledArg {
6015 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
6016 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
6017 value: ast::LiteralValue::Bool(true),
6018 raw: "true".to_string(),
6019 digest: None,
6020 }))),
6021 });
6022 }
6023 } else {
6024 call.arguments
6026 .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
6027 }
6028 }
6029 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6030 }
6031 }
6032 AstMutateCommand::EditArc {
6033 start,
6034 end,
6035 center,
6036 construction,
6037 } => {
6038 if let NodeMut::CallExpressionKw(call) = node {
6039 if call.callee.name.name != ARC_FN {
6040 return TraversalReturn::new_continue(());
6041 }
6042 for labeled_arg in &mut call.arguments {
6044 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_START_PARAM) {
6045 labeled_arg.arg = start.clone();
6046 }
6047 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_END_PARAM) {
6048 labeled_arg.arg = end.clone();
6049 }
6050 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_CENTER_PARAM) {
6051 labeled_arg.arg = center.clone();
6052 }
6053 }
6054 if let Some(construction_value) = construction {
6056 let construction_exists = call
6057 .arguments
6058 .iter()
6059 .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
6060 if *construction_value {
6061 if construction_exists {
6063 for labeled_arg in &mut call.arguments {
6065 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
6066 labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
6067 value: ast::LiteralValue::Bool(true),
6068 raw: "true".to_string(),
6069 digest: None,
6070 })));
6071 }
6072 }
6073 } else {
6074 call.arguments.push(ast::LabeledArg {
6076 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
6077 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
6078 value: ast::LiteralValue::Bool(true),
6079 raw: "true".to_string(),
6080 digest: None,
6081 }))),
6082 });
6083 }
6084 } else {
6085 call.arguments
6087 .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
6088 }
6089 }
6090 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6091 }
6092 }
6093 AstMutateCommand::EditCircle {
6094 start,
6095 center,
6096 construction,
6097 } => {
6098 if let NodeMut::CallExpressionKw(call) = node {
6099 if call.callee.name.name != CIRCLE_FN {
6100 return TraversalReturn::new_continue(());
6101 }
6102 for labeled_arg in &mut call.arguments {
6104 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CIRCLE_START_PARAM) {
6105 labeled_arg.arg = start.clone();
6106 }
6107 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CIRCLE_CENTER_PARAM) {
6108 labeled_arg.arg = center.clone();
6109 }
6110 }
6111 if let Some(construction_value) = construction {
6113 let construction_exists = call
6114 .arguments
6115 .iter()
6116 .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
6117 if *construction_value {
6118 if construction_exists {
6119 for labeled_arg in &mut call.arguments {
6121 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
6122 labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
6123 value: ast::LiteralValue::Bool(true),
6124 raw: "true".to_string(),
6125 digest: None,
6126 })));
6127 }
6128 }
6129 } else {
6130 call.arguments.push(ast::LabeledArg {
6132 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
6133 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
6134 value: ast::LiteralValue::Bool(true),
6135 raw: "true".to_string(),
6136 digest: None,
6137 }))),
6138 });
6139 }
6140 } else {
6141 call.arguments
6143 .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
6144 }
6145 }
6146 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6147 }
6148 }
6149 AstMutateCommand::EditControlPointSpline { points, construction } => {
6150 if let NodeMut::CallExpressionKw(call) = node {
6151 if call.callee.name.name != CONTROL_POINT_SPLINE_FN {
6152 return TraversalReturn::new_continue(());
6153 }
6154 for labeled_arg in &mut call.arguments {
6155 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONTROL_POINT_SPLINE_POINTS_PARAM)
6156 {
6157 labeled_arg.arg = points.clone();
6158 }
6159 }
6160 if let Some(construction_value) = construction {
6162 let construction_exists = call
6163 .arguments
6164 .iter()
6165 .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
6166 if *construction_value {
6167 if construction_exists {
6168 for labeled_arg in &mut call.arguments {
6169 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
6170 labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
6171 value: ast::LiteralValue::Bool(true),
6172 raw: "true".to_string(),
6173 digest: None,
6174 })));
6175 }
6176 }
6177 } else {
6178 call.arguments.push(ast::LabeledArg {
6179 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
6180 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
6181 value: ast::LiteralValue::Bool(true),
6182 raw: "true".to_string(),
6183 digest: None,
6184 }))),
6185 });
6186 }
6187 } else {
6188 call.arguments
6189 .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
6190 }
6191 }
6192 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6193 }
6194 }
6195 AstMutateCommand::EditConstraintValue { value } => {
6196 if let NodeMut::BinaryExpression(binary_expr) = node {
6197 let left_is_constraint = matches!(
6198 &binary_expr.left,
6199 ast::BinaryPart::CallExpressionKw(call)
6200 if matches!(
6201 call.callee.name.name.as_str(),
6202 DISTANCE_FN | HORIZONTAL_DISTANCE_FN | VERTICAL_DISTANCE_FN | RADIUS_FN | DIAMETER_FN | ANGLE_FN
6203 )
6204 );
6205 if left_is_constraint {
6206 binary_expr.right = value.clone();
6207 } else {
6208 binary_expr.left = value.clone();
6209 }
6210
6211 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6212 }
6213 }
6214 AstMutateCommand::EditDistanceConstraintLabelPosition { label_position } => {
6215 if let NodeMut::BinaryExpression(binary_expr) = node {
6216 let ast::BinaryPart::CallExpressionKw(call) = &mut binary_expr.left else {
6217 return TraversalReturn::new_continue(());
6218 };
6219 if !matches!(
6220 call.callee.name.name.as_str(),
6221 DISTANCE_FN | HORIZONTAL_DISTANCE_FN | VERTICAL_DISTANCE_FN | RADIUS_FN | DIAMETER_FN
6222 ) {
6223 return TraversalReturn::new_continue(());
6224 }
6225
6226 if let Some(label_arg) = call
6227 .arguments
6228 .iter_mut()
6229 .find(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(LABEL_POSITION_PARAM))
6230 {
6231 label_arg.arg = label_position.clone();
6232 } else {
6233 call.arguments.push(ast::LabeledArg {
6234 label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
6235 arg: label_position.clone(),
6236 });
6237 }
6238
6239 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6240 }
6241 }
6242 AstMutateCommand::EditCallUnlabeled { arg } => {
6243 if let NodeMut::CallExpressionKw(call) = node {
6244 call.unlabeled = Some(arg.clone());
6245 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6246 }
6247 }
6248 AstMutateCommand::EditVarInitialValue { value } => {
6249 if let NodeMut::SketchVar(sketch_var) = node {
6253 let Ok(literal) = to_source_number(*value) else {
6254 return TraversalReturn::new_break(Err(KclError::refactor(format!(
6255 "Could not convert number to AST literal: {:?}",
6256 *value
6257 ))));
6258 };
6259 sketch_var.initial = Some(Box::new(ast::Node::no_src(literal)));
6260 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6261 }
6262 }
6263 AstMutateCommand::DeleteNode => {
6264 return TraversalReturn {
6265 mutate_body_item: MutateBodyItem::Delete,
6266 control_flow: ControlFlow::Break(Ok(AstMutateCommandReturn::None)),
6267 };
6268 }
6269 }
6270 TraversalReturn::new_continue(())
6271}
6272
6273struct FindSketchBlockSourceRange {
6274 target_before_mutation: SourceRange,
6276 found: Cell<Option<AstNodeRef>>,
6280}
6281
6282impl<'a> crate::walk::Visitor<'a> for &FindSketchBlockSourceRange {
6283 type Error = crate::front::Error;
6284
6285 fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
6286 let Ok(node_range) = SourceRange::try_from(&node) else {
6287 return Ok(true);
6288 };
6289
6290 if let crate::walk::Node::SketchBlock(sketch_block) = node {
6291 if node_range.module_id() == self.target_before_mutation.module_id()
6292 && node_range.start() == self.target_before_mutation.start()
6293 && node_range.end() >= self.target_before_mutation.end()
6295 {
6296 self.found.set(sketch_block.body.items.last().map(|item| match item {
6297 ast::BodyItem::VariableDeclaration(node) => AstNodeRef::from(&node.declaration.init),
6301 _ => AstNodeRef::from(item),
6302 }));
6303 return Ok(false);
6304 } else {
6305 return Ok(true);
6308 }
6309 }
6310
6311 for child in node.children().iter() {
6312 if !child.visit(*self)? {
6313 return Ok(false);
6314 }
6315 }
6316
6317 Ok(true)
6318 }
6319}
6320
6321struct FindSketchBlockByNodePath {
6322 target_node_path: ast::NodePath,
6324 found: Cell<Option<AstNodeRef>>,
6328}
6329
6330impl<'a> crate::walk::Visitor<'a> for &FindSketchBlockByNodePath {
6331 type Error = crate::front::Error;
6332
6333 fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
6334 let Ok(node_path) = <Option<ast::NodePath>>::try_from(&node) else {
6335 return Ok(true);
6336 };
6337
6338 if let crate::walk::Node::SketchBlock(sketch_block) = node {
6339 if let Some(node_path) = node_path
6340 && node_path == self.target_node_path
6341 {
6342 self.found.set(sketch_block.body.items.last().map(|item| match item {
6343 ast::BodyItem::VariableDeclaration(node) => AstNodeRef::from(&node.declaration.init),
6347 _ => AstNodeRef::from(item),
6348 }));
6349
6350 return Ok(false);
6351 } else {
6352 return Ok(true);
6355 }
6356 }
6357
6358 for child in node.children().iter() {
6359 if !child.visit(*self)? {
6360 return Ok(false);
6361 }
6362 }
6363
6364 Ok(true)
6365 }
6366}
6367
6368fn find_sketch_block_added_item(
6376 ast: &ast::Node<ast::Program>,
6377 sketch_block_before_mutation: &AstNodeRef,
6378) -> Result<AstNodeRef, KclError> {
6379 if let Some(node_path) = &sketch_block_before_mutation.node_path {
6380 let find = FindSketchBlockByNodePath {
6381 target_node_path: node_path.clone(),
6382 found: Cell::new(None),
6383 };
6384 let node = crate::walk::Node::from(ast);
6385 node.visit(&find).map_err(|err| KclError::refactor(err.msg))?;
6386 find.found.into_inner().ok_or_else(|| {
6387 KclError::refactor(format!(
6388 "Node ID after mutation not found for Node ID before mutation: {node_path:?}"
6389 ))
6390 })
6391 } else {
6392 let find = FindSketchBlockSourceRange {
6394 target_before_mutation: sketch_block_before_mutation.range,
6395 found: Cell::new(None),
6396 };
6397 let node = crate::walk::Node::from(ast);
6398 node.visit(&find).map_err(|err| KclError::refactor(err.msg))?;
6399 find.found.into_inner().ok_or_else(|| KclError::refactor(
6400 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?"),
6401 ))
6402 }
6403}
6404
6405fn format_kcl_error_message(prefix: &str, error: &KclError) -> String {
6406 let message = error.message().trim();
6407 let message = if message.is_empty() {
6408 "unknown parse error"
6409 } else {
6410 message
6411 };
6412
6413 format!("{prefix}: {message}")
6414}
6415
6416fn parse_frontend_mutation_source(source: &str, parse_error_prefix: &str, no_ast_message: &str) -> ExecResult<Program> {
6417 let (program, errors) = Program::parse(source).map_err(|err| {
6418 KclErrorWithOutputs::no_outputs(KclError::refactor(format_kcl_error_message(parse_error_prefix, &err)))
6419 })?;
6420 if !errors.is_empty() {
6421 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
6422 format_compilation_issues(parse_error_prefix, &errors),
6423 )));
6424 }
6425
6426 program.ok_or_else(|| KclErrorWithOutputs::no_outputs(KclError::refactor(no_ast_message.to_owned())))
6427}
6428
6429fn format_compilation_issues(prefix: &str, issues: &[CompilationIssue]) -> String {
6430 let Some(first_issue) = issues
6431 .iter()
6432 .find(|issue| issue.severity.is_err())
6433 .or_else(|| issues.first())
6434 else {
6435 return prefix.to_owned();
6436 };
6437
6438 let message = first_issue.message.trim();
6439 let message = if message.is_empty() {
6440 "unknown parse error"
6441 } else {
6442 message
6443 };
6444
6445 if issues.len() > 1 {
6446 format!("{prefix}: {message} (+{} more)", issues.len() - 1)
6447 } else {
6448 format!("{prefix}: {message}")
6449 }
6450}
6451
6452fn source_from_ast(ast: &ast::Node<ast::Program>) -> String {
6453 ast.recast_top(&Default::default(), 0)
6455}
6456
6457struct FindNumericLiteral {
6458 target: SourceRange,
6459 found: Cell<Option<ast::NumericLiteral>>,
6460}
6461
6462impl<'a> crate::walk::Visitor<'a> for &FindNumericLiteral {
6463 type Error = crate::front::Error;
6464
6465 fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
6466 let Ok(node_range) = SourceRange::try_from(&node) else {
6467 return Ok(true);
6468 };
6469
6470 if node_range == self.target
6471 && let crate::walk::Node::NumericLiteral(literal) = node
6472 {
6473 self.found.set(Some(literal.inner.clone()));
6474 return Ok(false);
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 numeric_literal_at_source_range(ast: &ast::Node<ast::Program>, target: SourceRange) -> Option<ast::NumericLiteral> {
6488 let find = FindNumericLiteral {
6489 target,
6490 found: Cell::new(None),
6491 };
6492 let node = crate::walk::Node::from(ast);
6493 node.visit(&find).ok()?;
6494 find.found.into_inner()
6495}
6496
6497struct FindSketchVarInitialByNodePath<'a> {
6498 target: &'a ast::NodePath,
6499 sketch_var_found: Cell<bool>,
6500 initial_literal: Cell<Option<ast::NumericLiteral>>,
6501}
6502
6503impl<'a, 'b> crate::walk::Visitor<'b> for &FindSketchVarInitialByNodePath<'a> {
6504 type Error = crate::front::Error;
6505
6506 fn visit_node(&self, node: crate::walk::Node<'b>) -> anyhow::Result<bool, Self::Error> {
6507 if let crate::walk::Node::SketchVar(sketch_var) = node
6508 && sketch_var.node_path.as_ref() == Some(self.target)
6509 {
6510 self.sketch_var_found.set(true);
6511 if let Some(initial) = &sketch_var.initial {
6512 self.initial_literal.set(Some(initial.inner.clone()));
6513 }
6514 return Ok(false);
6515 }
6516
6517 for child in node.children().iter() {
6518 if !child.visit(*self)? {
6519 return Ok(false);
6520 }
6521 }
6522
6523 Ok(true)
6524 }
6525}
6526
6527fn numeric_literal_at_node_path(
6537 ast: &ast::Node<ast::Program>,
6538 node_path: Option<&ast::NodePath>,
6539 source_range: SourceRange,
6540) -> Option<Option<ast::NumericLiteral>> {
6541 let Some(node_path) = node_path else {
6542 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";
6543 #[cfg(target_arch = "wasm32")]
6544 web_sys::console::warn_1(&message.into());
6545 #[cfg(not(target_arch = "wasm32"))]
6546 eprintln!("WARNING: {message}");
6547 return numeric_literal_at_source_range(ast, source_range).map(Some);
6548 };
6549 let find = FindSketchVarInitialByNodePath {
6550 target: node_path,
6551 sketch_var_found: Cell::new(false),
6552 initial_literal: Cell::new(None),
6553 };
6554 let node = crate::walk::Node::from(ast);
6555 node.visit(&find).ok()?;
6556 if !find.sketch_var_found.get() {
6557 return None;
6558 }
6559 Some(find.initial_literal.into_inner())
6560}
6561
6562fn suffix_length_unit(suffix: NumericSuffix) -> Option<UnitLength> {
6563 match suffix {
6564 NumericSuffix::Mm => Some(UnitLength::Millimeters),
6565 NumericSuffix::Cm => Some(UnitLength::Centimeters),
6566 NumericSuffix::M => Some(UnitLength::Meters),
6567 NumericSuffix::Inch => Some(UnitLength::Inches),
6568 NumericSuffix::Ft => Some(UnitLength::Feet),
6569 NumericSuffix::Yd => Some(UnitLength::Yards),
6570 _ => None,
6571 }
6572}
6573
6574fn number_value_in_default_length_units(number: Number, default_length_unit: UnitLength) -> f64 {
6575 match suffix_length_unit(number.units) {
6576 Some(unit) => adjust_length(unit, number.value, default_length_unit).0,
6577 None => number.value,
6578 }
6579}
6580
6581fn literal_value_in_default_length_units(literal: &ast::NumericLiteral, default_length_unit: UnitLength) -> f64 {
6582 match suffix_length_unit(literal.suffix) {
6583 Some(unit) => adjust_length(unit, literal.value, default_length_unit).0,
6584 None => literal.value,
6585 }
6586}
6587
6588fn var_solution_needs_commit(
6589 current_literal: &ast::NumericLiteral,
6590 solved_value: Number,
6591 default_length_unit: UnitLength,
6592) -> bool {
6593 let current = literal_value_in_default_length_units(current_literal, default_length_unit);
6594 let solved = number_value_in_default_length_units(solved_value, default_length_unit);
6595
6596 (current - solved).abs() > 1e-9
6597}
6598
6599fn preserve_var_solution_literal_style(
6600 current_literal: &ast::NumericLiteral,
6601 solved_value: Number,
6602 default_length_unit: UnitLength,
6603) -> Number {
6604 if current_literal.suffix == NumericSuffix::None {
6605 return Number {
6606 value: number_value_in_default_length_units(solved_value, default_length_unit),
6607 units: NumericSuffix::None,
6608 };
6609 }
6610
6611 let Some(current_unit) = suffix_length_unit(current_literal.suffix) else {
6612 return solved_value;
6613 };
6614
6615 let solved_default_value = number_value_in_default_length_units(solved_value, default_length_unit);
6616 Number {
6617 value: adjust_length(default_length_unit, solved_default_value, current_unit).0,
6618 units: current_literal.suffix,
6619 }
6620}
6621
6622pub(crate) fn to_ast_point2d(point: &Point2d<Expr>) -> anyhow::Result<ast::Expr> {
6623 Ok(ast::Expr::ArrayExpression(Box::new(ast::Node {
6624 inner: ast::ArrayExpression {
6625 elements: vec![to_source_expr(&point.x)?, to_source_expr(&point.y)?],
6626 non_code_meta: Default::default(),
6627 digest: None,
6628 },
6629 start: Default::default(),
6630 end: Default::default(),
6631 module_id: Default::default(),
6632 node_path: None,
6633 outer_attrs: Default::default(),
6634 pre_comments: Default::default(),
6635 comment_start: Default::default(),
6636 })))
6637}
6638
6639pub(crate) fn to_ast_point2d_array(points: &[Point2d<Expr>]) -> anyhow::Result<ast::Expr> {
6640 Ok(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
6641 ast::ArrayExpression {
6642 elements: points.iter().map(to_ast_point2d).collect::<anyhow::Result<Vec<_>>>()?,
6643 digest: None,
6644 non_code_meta: Default::default(),
6645 },
6646 ))))
6647}
6648
6649fn to_ast_point2d_number(point: &Point2d<Number>) -> anyhow::Result<ast::Expr> {
6650 Ok(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
6651 ast::ArrayExpression {
6652 elements: vec![
6653 ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(to_source_number(
6654 point.x,
6655 )?)))),
6656 ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(to_source_number(
6657 point.y,
6658 )?)))),
6659 ],
6660 non_code_meta: Default::default(),
6661 digest: None,
6662 },
6663 ))))
6664}
6665
6666fn to_source_expr(expr: &Expr) -> anyhow::Result<ast::Expr> {
6667 match expr {
6668 Expr::Number(number) => Ok(ast::Expr::Literal(Box::new(ast::Node {
6669 inner: ast::Literal::from(to_source_number(*number)?),
6670 start: Default::default(),
6671 end: Default::default(),
6672 module_id: Default::default(),
6673 node_path: None,
6674 outer_attrs: Default::default(),
6675 pre_comments: Default::default(),
6676 comment_start: Default::default(),
6677 }))),
6678 Expr::Var(number) => Ok(ast::Expr::SketchVar(Box::new(ast::Node {
6679 inner: ast::SketchVar {
6680 initial: Some(Box::new(ast::Node {
6681 inner: to_source_number(*number)?,
6682 start: Default::default(),
6683 end: Default::default(),
6684 module_id: Default::default(),
6685 node_path: None,
6686 outer_attrs: Default::default(),
6687 pre_comments: Default::default(),
6688 comment_start: Default::default(),
6689 })),
6690 digest: None,
6691 },
6692 start: Default::default(),
6693 end: Default::default(),
6694 module_id: Default::default(),
6695 node_path: None,
6696 outer_attrs: Default::default(),
6697 pre_comments: Default::default(),
6698 comment_start: Default::default(),
6699 }))),
6700 Expr::Variable(variable) => Ok(ast_name_expr(variable.clone())),
6701 }
6702}
6703
6704fn to_source_number(number: Number) -> anyhow::Result<ast::NumericLiteral> {
6705 Ok(ast::NumericLiteral {
6706 value: number.value,
6707 suffix: number.units,
6708 raw: format_number_literal(number.value, number.units, None)?,
6709 digest: None,
6710 })
6711}
6712
6713pub(crate) fn ast_name_expr(name: String) -> ast::Expr {
6714 ast::Expr::Name(Box::new(ast_name(name)))
6715}
6716
6717fn ast_name(name: String) -> ast::Node<ast::Name> {
6718 ast::Node {
6719 inner: ast::Name {
6720 name: ast::Node {
6721 inner: ast::Identifier { name, digest: None },
6722 start: Default::default(),
6723 end: Default::default(),
6724 module_id: Default::default(),
6725 node_path: None,
6726 outer_attrs: Default::default(),
6727 pre_comments: Default::default(),
6728 comment_start: Default::default(),
6729 },
6730 path: Vec::new(),
6731 abs_path: false,
6732 digest: None,
6733 },
6734 start: Default::default(),
6735 end: Default::default(),
6736 module_id: Default::default(),
6737 node_path: None,
6738 outer_attrs: Default::default(),
6739 pre_comments: Default::default(),
6740 comment_start: Default::default(),
6741 }
6742}
6743
6744pub(crate) fn ast_sketch2_name(name: &str) -> ast::Name {
6745 ast::Name {
6746 name: ast::Node {
6747 inner: ast::Identifier {
6748 name: name.to_owned(),
6749 digest: None,
6750 },
6751 start: Default::default(),
6752 end: Default::default(),
6753 module_id: Default::default(),
6754 node_path: None,
6755 outer_attrs: Default::default(),
6756 pre_comments: Default::default(),
6757 comment_start: Default::default(),
6758 },
6759 path: Default::default(),
6760 abs_path: false,
6761 digest: None,
6762 }
6763}
6764
6765pub(crate) fn create_coincident_ast(exprs: impl IntoIterator<Item = ast::Expr>) -> ast::Expr {
6769 let elements = exprs.into_iter().collect::<Vec<_>>();
6770 debug_assert!(elements.len() >= 2, "Coincident AST should have at least 2 inputs");
6771
6772 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
6774 elements,
6775 digest: None,
6776 non_code_meta: Default::default(),
6777 })));
6778
6779 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6781 callee: ast::Node::no_src(ast_sketch2_name(COINCIDENT_FN)),
6782 unlabeled: Some(array_expr),
6783 arguments: Default::default(),
6784 digest: None,
6785 non_code_meta: Default::default(),
6786 })))
6787}
6788
6789pub(crate) fn create_line_ast(start_ast: ast::Expr, end_ast: ast::Expr) -> ast::Expr {
6791 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6792 callee: ast::Node::no_src(ast_sketch2_name(LINE_FN)),
6793 unlabeled: None,
6794 arguments: vec![
6795 ast::LabeledArg {
6796 label: Some(ast::Identifier::new(LINE_START_PARAM)),
6797 arg: start_ast,
6798 },
6799 ast::LabeledArg {
6800 label: Some(ast::Identifier::new(LINE_END_PARAM)),
6801 arg: end_ast,
6802 },
6803 ],
6804 digest: None,
6805 non_code_meta: Default::default(),
6806 })))
6807}
6808
6809pub(crate) fn create_arc_ast(start_ast: ast::Expr, end_ast: ast::Expr, center_ast: ast::Expr) -> ast::Expr {
6811 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6812 callee: ast::Node::no_src(ast_sketch2_name(ARC_FN)),
6813 unlabeled: None,
6814 arguments: vec![
6815 ast::LabeledArg {
6816 label: Some(ast::Identifier::new(ARC_START_PARAM)),
6817 arg: start_ast,
6818 },
6819 ast::LabeledArg {
6820 label: Some(ast::Identifier::new(ARC_END_PARAM)),
6821 arg: end_ast,
6822 },
6823 ast::LabeledArg {
6824 label: Some(ast::Identifier::new(ARC_CENTER_PARAM)),
6825 arg: center_ast,
6826 },
6827 ],
6828 digest: None,
6829 non_code_meta: Default::default(),
6830 })))
6831}
6832
6833pub(crate) fn create_circle_ast(start_ast: ast::Expr, center_ast: ast::Expr) -> ast::Expr {
6835 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6836 callee: ast::Node::no_src(ast_sketch2_name(CIRCLE_FN)),
6837 unlabeled: None,
6838 arguments: vec![
6839 ast::LabeledArg {
6840 label: Some(ast::Identifier::new(CIRCLE_START_PARAM)),
6841 arg: start_ast,
6842 },
6843 ast::LabeledArg {
6844 label: Some(ast::Identifier::new(CIRCLE_CENTER_PARAM)),
6845 arg: center_ast,
6846 },
6847 ],
6848 digest: None,
6849 non_code_meta: Default::default(),
6850 })))
6851}
6852
6853pub(crate) fn create_horizontal_ast(line_expr: ast::Expr) -> ast::Expr {
6855 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6856 callee: ast::Node::no_src(ast_sketch2_name(HORIZONTAL_FN)),
6857 unlabeled: Some(line_expr),
6858 arguments: Default::default(),
6859 digest: None,
6860 non_code_meta: Default::default(),
6861 })))
6862}
6863
6864pub(crate) fn create_vertical_ast(line_expr: ast::Expr) -> ast::Expr {
6866 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6867 callee: ast::Node::no_src(ast_sketch2_name(VERTICAL_FN)),
6868 unlabeled: Some(line_expr),
6869 arguments: Default::default(),
6870 digest: None,
6871 non_code_meta: Default::default(),
6872 })))
6873}
6874
6875pub(crate) fn create_member_expression(object_expr: ast::Expr, property: &str) -> ast::Expr {
6877 ast::Expr::MemberExpression(Box::new(ast::Node::no_src(ast::MemberExpression {
6878 object: object_expr,
6879 property: ast::Expr::Name(Box::new(ast::Node::no_src(ast::Name {
6880 name: ast::Node::no_src(ast::Identifier {
6881 name: property.to_string(),
6882 digest: None,
6883 }),
6884 path: Vec::new(),
6885 abs_path: false,
6886 digest: None,
6887 }))),
6888 computed: false,
6889 digest: None,
6890 })))
6891}
6892
6893pub(crate) fn create_index_expression(object_expr: ast::Expr, index: usize) -> ast::Expr {
6894 ast::Expr::MemberExpression(Box::new(ast::Node::no_src(ast::MemberExpression {
6895 object: object_expr,
6896 property: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(ast::NumericLiteral {
6897 value: index as f64,
6898 suffix: NumericSuffix::None,
6899 raw: index.to_string(),
6900 digest: None,
6901 })))),
6902 computed: true,
6903 digest: None,
6904 })))
6905}
6906
6907fn create_fixed_point_constraint_ast(point_expr: ast::Expr, position: Point2d<Number>) -> anyhow::Result<ast::Expr> {
6909 let x_literal = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(to_source_number(
6911 position.x,
6912 )?))));
6913 let y_literal = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(to_source_number(
6914 position.y,
6915 )?))));
6916 let point_array = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
6917 elements: vec![x_literal, y_literal],
6918 digest: None,
6919 non_code_meta: Default::default(),
6920 })));
6921
6922 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
6924 elements: vec![point_expr, point_array],
6925 digest: None,
6926 non_code_meta: Default::default(),
6927 })));
6928
6929 Ok(ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(
6931 ast::CallExpressionKw {
6932 callee: ast::Node::no_src(ast_sketch2_name(FIXED_FN)),
6933 unlabeled: Some(array_expr),
6934 arguments: Default::default(),
6935 digest: None,
6936 non_code_meta: Default::default(),
6937 },
6938 ))))
6939}
6940
6941pub(crate) fn create_equal_length_ast(line_exprs: Vec<ast::Expr>) -> ast::Expr {
6943 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
6944 elements: line_exprs,
6945 digest: None,
6946 non_code_meta: Default::default(),
6947 })));
6948
6949 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6951 callee: ast::Node::no_src(ast_sketch2_name(EQUAL_LENGTH_FN)),
6952 unlabeled: Some(array_expr),
6953 arguments: Default::default(),
6954 digest: None,
6955 non_code_meta: Default::default(),
6956 })))
6957}
6958
6959pub(crate) fn create_equal_radius_ast(segment_exprs: Vec<ast::Expr>) -> ast::Expr {
6961 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
6962 elements: segment_exprs,
6963 digest: None,
6964 non_code_meta: Default::default(),
6965 })));
6966
6967 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6968 callee: ast::Node::no_src(ast_sketch2_name(EQUAL_RADIUS_FN)),
6969 unlabeled: Some(array_expr),
6970 arguments: Default::default(),
6971 digest: None,
6972 non_code_meta: Default::default(),
6973 })))
6974}
6975
6976pub(crate) fn create_tangent_ast(seg1_expr: ast::Expr, seg2_expr: ast::Expr) -> ast::Expr {
6978 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
6979 elements: vec![seg1_expr, seg2_expr],
6980 digest: None,
6981 non_code_meta: Default::default(),
6982 })));
6983
6984 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6985 callee: ast::Node::no_src(ast_sketch2_name(TANGENT_FN)),
6986 unlabeled: Some(array_expr),
6987 arguments: Default::default(),
6988 digest: None,
6989 non_code_meta: Default::default(),
6990 })))
6991}
6992
6993pub(crate) fn create_symmetric_ast(input_exprs: Vec<ast::Expr>, axis_expr: ast::Expr) -> ast::Expr {
6995 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
6996 elements: input_exprs,
6997 digest: None,
6998 non_code_meta: Default::default(),
6999 })));
7000 let arguments = vec![ast::LabeledArg {
7001 label: Some(ast::Identifier::new(SYMMETRIC_AXIS_PARAM)),
7002 arg: axis_expr,
7003 }];
7004
7005 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
7006 callee: ast::Node::no_src(ast_sketch2_name(SYMMETRIC_FN)),
7007 unlabeled: Some(array_expr),
7008 arguments,
7009 digest: None,
7010 non_code_meta: Default::default(),
7011 })))
7012}
7013
7014pub(crate) fn create_midpoint_ast(segment_expr: ast::Expr, point_expr: ast::Expr) -> ast::Expr {
7016 let arguments = vec![ast::LabeledArg {
7017 label: Some(ast::Identifier::new(MIDPOINT_POINT_PARAM)),
7018 arg: point_expr,
7019 }];
7020
7021 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
7022 callee: ast::Node::no_src(ast_sketch2_name(MIDPOINT_FN)),
7023 unlabeled: Some(segment_expr),
7024 arguments,
7025 digest: None,
7026 non_code_meta: Default::default(),
7027 })))
7028}
7029
7030#[cfg(test)]
7031mod tests {
7032 use std::sync;
7033
7034 use super::*;
7035 use crate::engine::PlaneName;
7036 use crate::engine::engine_manager::EngineManager;
7037 use crate::execution::cache::SketchModeState;
7038 use crate::execution::cache::clear_mem_cache;
7039 use crate::execution::cache::read_old_memory;
7040 use crate::execution::cache::write_old_memory;
7041 use crate::front::Distance;
7042 use crate::front::Fixed;
7043 use crate::front::FixedPoint;
7044 use crate::front::Midpoint;
7045 use crate::front::Object;
7046 use crate::front::Plane;
7047 use crate::front::Sketch;
7048 use crate::front::Tangent;
7049 use crate::frontend::sketch::Vertical;
7050 use crate::pretty::NumericSuffix;
7051
7052 fn find_first_sketch_object(scene_graph: &SceneGraph) -> Option<&Object> {
7053 for object in &scene_graph.objects {
7054 if let ObjectKind::Sketch(_) = &object.kind {
7055 return Some(object);
7056 }
7057 }
7058 None
7059 }
7060
7061 fn find_first_face_object(scene_graph: &SceneGraph) -> Option<&Object> {
7062 for object in &scene_graph.objects {
7063 if let ObjectKind::Face(_) = &object.kind {
7064 return Some(object);
7065 }
7066 }
7067 None
7068 }
7069
7070 fn find_first_wall_object_id(scene_graph: &SceneGraph) -> Option<ObjectId> {
7071 for object in &scene_graph.objects {
7072 if matches!(&object.kind, ObjectKind::Wall(_)) {
7073 return Some(object.id);
7074 }
7075 }
7076 None
7077 }
7078
7079 fn find_cap_object_id_with_solid_output_index(
7080 scene_graph: &SceneGraph,
7081 cap_kind: crate::frontend::api::CapKind,
7082 solid_output_index: usize,
7083 ) -> Option<ObjectId> {
7084 for object in &scene_graph.objects {
7085 if matches!(&object.kind, ObjectKind::Cap(cap) if cap.kind == cap_kind && cap.solid_output_index == Some(solid_output_index))
7086 {
7087 return Some(object.id);
7088 }
7089 }
7090 None
7091 }
7092
7093 #[test]
7094 fn test_region_name_from_sweep_variable_supports_sweep_kinds() {
7095 let source = "\
7096region001 = region(point = [0.1, 0.1], sketch = s)
7097extrude001 = extrude(region001, length = 5)
7098revolve001 = revolve(region001, axis = Y)
7099sweep001 = sweep(region001, path = path001)
7100loft001 = loft(region001)
7101not_sweep001 = shell(extrude001, faces = [], thickness = 1)
7102";
7103
7104 let program = Program::parse(source).unwrap().0.unwrap();
7105
7106 assert_eq!(
7107 region_name_from_sweep_variable(&program.ast, "extrude001"),
7108 Some("region001".to_owned())
7109 );
7110 assert_eq!(
7111 region_name_from_sweep_variable(&program.ast, "revolve001"),
7112 Some("region001".to_owned())
7113 );
7114 assert_eq!(
7115 region_name_from_sweep_variable(&program.ast, "sweep001"),
7116 Some("region001".to_owned())
7117 );
7118 assert_eq!(
7119 region_name_from_sweep_variable(&program.ast, "loft001"),
7120 Some("region001".to_owned())
7121 );
7122 assert_eq!(region_name_from_sweep_variable(&program.ast, "not_sweep001"), None);
7123 }
7124
7125 #[track_caller]
7126 fn expect_sketch(object: &Object) -> &Sketch {
7127 if let ObjectKind::Sketch(sketch) = &object.kind {
7128 sketch
7129 } else {
7130 panic!("Object is not a sketch: {:?}", object);
7131 }
7132 }
7133
7134 fn point_position(scene_graph: &SceneGraph, point_id: ObjectId) -> Point2d<Number> {
7135 let point_object = scene_graph.objects.get(point_id.0).unwrap();
7136 let ObjectKind::Segment {
7137 segment: Segment::Point(point),
7138 } = &point_object.kind
7139 else {
7140 panic!("Object is not a point segment: {point_object:?}");
7141 };
7142 point.position.clone()
7143 }
7144
7145 fn assert_point_position_close(actual: Point2d<Number>, expected: Point2d<Number>) {
7146 assert!((actual.x.value - expected.x.value).abs() < 1e-6);
7147 assert!((actual.y.value - expected.y.value).abs() < 1e-6);
7148 }
7149
7150 fn point_expr_mm(x: f64, y: f64) -> Point2d<Expr> {
7153 Point2d {
7154 x: Expr::Var(Number {
7155 value: x,
7156 units: NumericSuffix::Mm,
7157 }),
7158 y: Expr::Var(Number {
7159 value: y,
7160 units: NumericSuffix::Mm,
7161 }),
7162 }
7163 }
7164
7165 fn point_number_mm(x: f64, y: f64) -> Point2d<Number> {
7168 Point2d {
7169 x: Number {
7170 value: x,
7171 units: NumericSuffix::Mm,
7172 },
7173 y: Number {
7174 value: y,
7175 units: NumericSuffix::Mm,
7176 },
7177 }
7178 }
7179
7180 fn make_line_ctor(start_x: f64, start_y: f64, end_x: f64, end_y: f64, units: NumericSuffix) -> LineCtor {
7181 LineCtor {
7182 start: Point2d {
7183 x: Expr::Number(Number { value: start_x, units }),
7184 y: Expr::Number(Number { value: start_y, units }),
7185 },
7186 end: Point2d {
7187 x: Expr::Number(Number { value: end_x, units }),
7188 y: Expr::Number(Number { value: end_y, units }),
7189 },
7190 construction: None,
7191 }
7192 }
7193
7194 async fn create_sketch_with_single_line(
7195 frontend: &mut FrontendState,
7196 ctx: &ExecutorContext,
7197 mock_ctx: &ExecutorContext,
7198 version: Version,
7199 ) -> (ObjectId, ObjectId, SourceDelta, SceneGraphDelta) {
7200 frontend.program = Program::empty();
7201
7202 let sketch_args = SketchCtor {
7203 on: Plane::Default(PlaneName::Xy),
7204 };
7205 let (_src_delta, _scene_delta, sketch_id) = frontend
7206 .new_sketch(ctx, ProjectId(0), FileId(0), version, sketch_args)
7207 .await
7208 .unwrap();
7209
7210 let segment = SegmentCtor::Line(make_line_ctor(0.0, 0.0, 10.0, 10.0, NumericSuffix::Mm));
7211 let (source_delta, scene_graph_delta) = frontend
7212 .add_segment(mock_ctx, version, sketch_id, segment, None)
7213 .await
7214 .unwrap();
7215 let line_id = *scene_graph_delta
7216 .new_objects
7217 .last()
7218 .expect("Expected line object id to be created");
7219
7220 (sketch_id, line_id, source_delta, scene_graph_delta)
7221 }
7222
7223 async fn seed_frontend_with_mock(frontend: &mut FrontendState, mock_ctx: &ExecutorContext, program: &Program) {
7224 frontend.program = program.clone();
7225 let outcome = mock_ctx.run_mock(program, &MockConfig::default()).await.unwrap();
7226 frontend.update_state_after_exec(outcome, true);
7227 }
7228
7229 #[test]
7230 fn test_parse_frontend_mutation_source_error_messages_are_user_facing() {
7231 for (source, expected_message) in [
7232 ("**", "Error parsing KCL source after editing: Unexpected token: *"),
7233 ("3'", "Error parsing KCL source after editing: found unknown token '''"),
7234 ] {
7235 let err = parse_frontend_mutation_source(
7236 source,
7237 "Error parsing KCL source after editing",
7238 "No AST produced after editing",
7239 )
7240 .expect_err("expected invalid KCL source to fail");
7241 let message = err.error.message();
7242
7243 assert_eq!(message, expected_message);
7244 assert!(!message.contains("CompilationIssue"));
7245 assert!(!message.contains("KclErrorDetails"));
7246 assert!(!message.contains("source_range"));
7247 }
7248 }
7249
7250 #[tokio::test(flavor = "multi_thread")]
7251 async fn test_edit_constraint_parse_error_messages_are_user_facing() {
7252 let initial_source = "\
7253sketch(on = XY) {
7254 line1 = line(start = [var 0, var 0], end = [var 10, var 0])
7255 distance([line1.start, line1.end]) == 10
7256}
7257";
7258 let program = Program::parse(initial_source).unwrap().0.unwrap();
7259
7260 let mut frontend = FrontendState::new();
7261 let mock_ctx = ExecutorContext::new_mock(None).await;
7262 let version = Version(0);
7263
7264 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
7265 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7266 let sketch_id = sketch_object.id;
7267 let sketch = expect_sketch(sketch_object);
7268 let constraint_id = *sketch.constraints.first().expect("expected distance constraint");
7269
7270 for (value, expected_message) in [
7271 ("**", "Invalid constraint value: Unexpected token: *"),
7272 ("3'", "Invalid constraint value: found unknown token '''"),
7273 ] {
7274 let err = frontend
7275 .edit_constraint(&mock_ctx, version, sketch_id, constraint_id, value.to_owned())
7276 .await
7277 .expect_err("expected invalid constraint expression to fail");
7278 let message = err.error.message();
7279
7280 assert_eq!(message, expected_message);
7281 assert!(!message.contains("CompilationIssue"));
7282 assert!(!message.contains("KclErrorDetails"));
7283 assert!(!message.contains("source_range"));
7284 }
7285
7286 mock_ctx.close().await;
7287 }
7288
7289 #[tokio::test(flavor = "multi_thread")]
7290 async fn test_failed_edit_constraint_does_not_update_program() {
7291 let initial_source = "\
7292sketch(on = XY) {
7293 line1 = line(start = [var 0, var 0], end = [var 10, var 0])
7294 distance([line1.start, line1.end]) == 10
7295}
7296";
7297 let program = Program::parse(initial_source).unwrap().0.unwrap();
7298 let original_source = program.original_file_contents.clone();
7299
7300 let mut frontend = FrontendState::new();
7301 let mock_ctx = ExecutorContext::new_mock(None).await;
7302 let version = Version(0);
7303
7304 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
7305 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7306 let sketch_id = sketch_object.id;
7307 let sketch = expect_sketch(sketch_object);
7308 let constraint_id = *sketch.constraints.first().expect("expected distance constraint");
7309
7310 frontend
7311 .edit_constraint(
7312 &mock_ctx,
7313 version,
7314 sketch_id,
7315 constraint_id,
7316 "unknownDistance".to_owned(),
7317 )
7318 .await
7319 .expect_err("expected invalid constraint value to fail execution");
7320
7321 assert_eq!(frontend.program.original_file_contents, original_source);
7322 assert!(!frontend.program.original_file_contents.contains("unknownDistance"));
7323
7324 mock_ctx.close().await;
7325 }
7326
7327 #[tokio::test(flavor = "multi_thread")]
7328 async fn test_edit_constraint_array_index_oob_fails_in_sketch_mode() {
7329 let initial_source = "\
7330arr = [0]
7331sketch(on = XY) {
7332 line1 = line(start = [var 0, var 0], end = [var 10, var 0])
7333 distance([line1.start, line1.end]) == 10
7334}
7335";
7336 let program = Program::parse(initial_source).unwrap().0.unwrap();
7337
7338 let mut frontend = FrontendState::new();
7339 let mock_ctx = ExecutorContext::new_mock(None).await;
7340 let version = Version(0);
7341
7342 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
7343 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7344 let sketch_id = sketch_object.id;
7345 let sketch = expect_sketch(sketch_object);
7346 let constraint_id = *sketch.constraints.first().expect("expected distance constraint");
7347
7348 let err = frontend
7352 .edit_constraint(&mock_ctx, version, sketch_id, constraint_id, "arr[5]".to_owned())
7353 .await
7354 .expect_err("expected out-of-bounds array index to be an error in sketch mode execution");
7355 let message = err.error.message();
7356 assert!(
7357 message.contains("The array doesn't have any item at index 5"),
7358 "unexpected error message: {message}"
7359 );
7360
7361 mock_ctx.close().await;
7362 }
7363
7364 #[tokio::test(flavor = "multi_thread")]
7365 async fn test_sketch_checkpoint_round_trip_restores_state() {
7366 let mut frontend = FrontendState::new();
7367 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7368 let mock_ctx = ExecutorContext::new_mock(None).await;
7369 let version = Version(0);
7370
7371 let (sketch_id, line_id, source_delta, scene_graph_delta) =
7372 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7373
7374 let expected_source = source_delta.text.clone();
7375 let expected_scene_graph = frontend.scene_graph.clone();
7376 let expected_exec_outcome = scene_graph_delta.exec_outcome.clone();
7377 let expected_point_freedom_cache = frontend.point_freedom_cache.clone();
7378
7379 let checkpoint_id = frontend
7380 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7381 .await
7382 .unwrap();
7383
7384 let edited_segments = vec![ExistingSegmentCtor {
7385 id: line_id,
7386 ctor: SegmentCtor::Line(make_line_ctor(1.0, 2.0, 13.0, 14.0, NumericSuffix::Mm)),
7387 }];
7388 let (edited_source, _edited_scene) = frontend
7389 .edit_segments(&mock_ctx, version, sketch_id, edited_segments)
7390 .await
7391 .unwrap();
7392 assert_ne!(edited_source.text, expected_source);
7393
7394 let restored = frontend.restore_sketch_checkpoint(checkpoint_id).await.unwrap();
7395
7396 assert_eq!(restored.source_delta.text, expected_source);
7397 assert_eq!(restored.scene_graph_delta.new_graph, expected_scene_graph);
7398 assert!(restored.scene_graph_delta.invalidates_ids);
7399 assert_eq!(restored.scene_graph_delta.exec_outcome, expected_exec_outcome);
7400 assert_eq!(frontend.scene_graph, expected_scene_graph);
7401 assert_eq!(frontend.point_freedom_cache, expected_point_freedom_cache);
7402
7403 ctx.close().await;
7404 }
7405
7406 #[tokio::test(flavor = "multi_thread")]
7407 async fn test_sketch_checkpoints_prune_oldest_entries() {
7408 let mut frontend = FrontendState::new();
7409 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7410 let mock_ctx = ExecutorContext::new_mock(None).await;
7411 let version = Version(0);
7412
7413 let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
7414 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7415
7416 let mut checkpoint_ids = Vec::new();
7417 for _ in 0..(MAX_SKETCH_CHECKPOINTS + 3) {
7418 checkpoint_ids.push(
7419 frontend
7420 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7421 .await
7422 .unwrap(),
7423 );
7424 }
7425
7426 assert_eq!(frontend.sketch_checkpoints.len(), MAX_SKETCH_CHECKPOINTS);
7427 assert!(checkpoint_ids.windows(2).all(|ids| ids[0] < ids[1]));
7428
7429 let oldest_retained = checkpoint_ids[3];
7430 assert_eq!(
7431 frontend.sketch_checkpoints.front().map(|checkpoint| checkpoint.id),
7432 Some(oldest_retained)
7433 );
7434
7435 let evicted_restore = frontend.restore_sketch_checkpoint(checkpoint_ids[0]).await;
7436 assert!(evicted_restore.is_err());
7437 assert!(evicted_restore.unwrap_err().msg.contains("Sketch checkpoint not found"));
7438
7439 frontend
7440 .restore_sketch_checkpoint(*checkpoint_ids.last().unwrap())
7441 .await
7442 .unwrap();
7443
7444 ctx.close().await;
7445 }
7446
7447 #[tokio::test(flavor = "multi_thread")]
7448 async fn test_restore_sketch_checkpoint_missing_id_returns_error() {
7449 let mut frontend = FrontendState::new();
7450 let missing_checkpoint = SketchCheckpointId::new(999);
7451
7452 let err = frontend
7453 .restore_sketch_checkpoint(missing_checkpoint)
7454 .await
7455 .expect_err("Expected restore to fail for missing checkpoint");
7456
7457 assert!(err.msg.contains("Sketch checkpoint not found"));
7458 }
7459
7460 #[tokio::test(flavor = "multi_thread")]
7461 async fn test_clear_sketch_checkpoints_removes_all_restore_points() {
7462 let mut frontend = FrontendState::new();
7463 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7464 let mock_ctx = ExecutorContext::new_mock(None).await;
7465 let version = Version(0);
7466
7467 let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
7468 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7469
7470 let checkpoint_a = frontend
7471 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7472 .await
7473 .unwrap();
7474 let checkpoint_b = frontend
7475 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7476 .await
7477 .unwrap();
7478 assert_eq!(frontend.sketch_checkpoints.len(), 2);
7479
7480 frontend.clear_sketch_checkpoints();
7481 assert!(frontend.sketch_checkpoints.is_empty());
7482 frontend.restore_sketch_checkpoint(checkpoint_a).await.unwrap_err();
7483 frontend.restore_sketch_checkpoint(checkpoint_b).await.unwrap_err();
7484
7485 ctx.close().await;
7486 }
7487
7488 #[tokio::test(flavor = "multi_thread")]
7489 async fn test_hack_set_program_keeps_old_checkpoints_and_adds_fresh_baseline() {
7490 let mut frontend = FrontendState::new();
7491 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7492 let mock_ctx = ExecutorContext::new_mock(None).await;
7493 let version = Version(0);
7494
7495 let (_sketch_id, _line_id, source_delta, scene_graph_delta) =
7496 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7497 let old_source = source_delta.text.clone();
7498 let old_checkpoint = frontend
7499 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7500 .await
7501 .unwrap();
7502 let initial_checkpoint_count = frontend.sketch_checkpoints.len();
7503
7504 let new_program = Program::parse("sketch(on = XY) {\n point(at = [1mm, 2mm])\n}\n")
7505 .unwrap()
7506 .0
7507 .unwrap();
7508
7509 let result = frontend.hack_set_program(&ctx, new_program).await.unwrap();
7510 let SetProgramOutcome::Success {
7511 checkpoint_id: Some(new_checkpoint),
7512 ..
7513 } = result
7514 else {
7515 panic!("Expected Success with a fresh checkpoint baseline");
7516 };
7517
7518 assert_eq!(frontend.sketch_checkpoints.len(), initial_checkpoint_count + 1);
7519
7520 let old_restore = frontend.restore_sketch_checkpoint(old_checkpoint).await.unwrap();
7521 assert_eq!(old_restore.source_delta.text, old_source);
7522
7523 let new_restore = frontend.restore_sketch_checkpoint(new_checkpoint).await.unwrap();
7524 assert!(new_restore.source_delta.text.contains("point(at = [1mm, 2mm])"));
7525
7526 ctx.close().await;
7527 }
7528
7529 #[tokio::test(flavor = "multi_thread")]
7530 async fn test_hack_set_program_exec_failure_does_not_add_checkpoint() {
7531 let mut frontend = FrontendState::new();
7532 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7533 let mock_ctx = ExecutorContext::new_mock(None).await;
7534 let version = Version(0);
7535
7536 let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
7537 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7538 let old_checkpoint = frontend
7539 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7540 .await
7541 .unwrap();
7542 let checkpoint_count_before = frontend.sketch_checkpoints.len();
7543
7544 let failing_program = Program::parse(
7545 "sketch(on = XY) {\n line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])\n}\n\nbad = missing_name\n",
7546 )
7547 .unwrap()
7548 .0
7549 .unwrap();
7550
7551 let result = frontend.hack_set_program(&ctx, failing_program).await.unwrap();
7552 assert!(matches!(result, SetProgramOutcome::ExecFailure { .. }));
7553 assert_eq!(frontend.sketch_checkpoints.len(), checkpoint_count_before);
7554 frontend.restore_sketch_checkpoint(old_checkpoint).await.unwrap();
7555
7556 ctx.close().await;
7557 }
7558
7559 #[tokio::test(flavor = "multi_thread")]
7560 async fn test_restore_sketch_checkpoint_restores_and_clears_mock_memory() {
7561 let mut frontend = FrontendState::new();
7562 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7563
7564 let program = Program::parse(
7565 "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",
7566 )
7567 .unwrap()
7568 .0
7569 .unwrap();
7570 let set_program_outcome = frontend.hack_set_program(&ctx, program).await.unwrap();
7571 let SetProgramOutcome::Success { exec_outcome, .. } = set_program_outcome else {
7572 panic!("Expected successful baseline program execution");
7573 };
7574
7575 clear_mem_cache().await;
7576 assert!(read_old_memory().await.is_none());
7577
7578 let checkpoint_without_mock_memory = frontend
7579 .create_sketch_checkpoint((*exec_outcome).clone())
7580 .await
7581 .unwrap();
7582
7583 write_old_memory(SketchModeState::new_for_tests()).await;
7584 assert!(read_old_memory().await.is_some());
7585
7586 let checkpoint_with_mock_memory = frontend
7587 .create_sketch_checkpoint((*exec_outcome).clone())
7588 .await
7589 .unwrap();
7590
7591 clear_mem_cache().await;
7592 assert!(read_old_memory().await.is_none());
7593
7594 frontend
7595 .restore_sketch_checkpoint(checkpoint_with_mock_memory)
7596 .await
7597 .unwrap();
7598 assert!(read_old_memory().await.is_some());
7599
7600 frontend
7601 .restore_sketch_checkpoint(checkpoint_without_mock_memory)
7602 .await
7603 .unwrap();
7604 assert!(read_old_memory().await.is_none());
7605
7606 ctx.close().await;
7607 }
7608
7609 #[tokio::test(flavor = "multi_thread")]
7610 async fn test_hack_set_program_exec_error_still_allows_edit_sketch() {
7611 let source = "\
7612sketch(on = XY) {
7613 line1 = line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])
7614}
7615
7616bad = missing_name
7617";
7618 let program = Program::parse(source).unwrap().0.unwrap();
7619
7620 let mut frontend = FrontendState::new();
7621
7622 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7623 let mock_ctx = ExecutorContext::new_mock(None).await;
7624 let version = Version(0);
7625 let project_id = ProjectId(0);
7626 let file_id = FileId(0);
7627
7628 let SetProgramOutcome::ExecFailure { .. } = frontend.hack_set_program(&ctx, program).await.unwrap() else {
7629 panic!("Expected ExecFailure from hack_set_program due to syntax error in program");
7630 };
7631
7632 let sketch_id = frontend
7633 .scene_graph
7634 .objects
7635 .iter()
7636 .find_map(|obj| matches!(obj.kind, ObjectKind::Sketch(_)).then_some(obj.id))
7637 .expect("Expected sketch object from errored hack_set_program");
7638
7639 frontend
7640 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
7641 .await
7642 .unwrap();
7643
7644 ctx.close().await;
7645 mock_ctx.close().await;
7646 }
7647
7648 #[tokio::test(flavor = "multi_thread")]
7649 async fn test_new_sketch_add_point_edit_point() {
7650 let program = Program::empty();
7651
7652 let mut frontend = FrontendState::new();
7653 frontend.program = program;
7654
7655 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7656 let mock_ctx = ExecutorContext::new_mock(None).await;
7657 let version = Version(0);
7658
7659 let sketch_args = SketchCtor {
7660 on: Plane::Default(PlaneName::Xy),
7661 };
7662 let (_src_delta, scene_delta, sketch_id) = frontend
7663 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
7664 .await
7665 .unwrap();
7666 assert_eq!(sketch_id, ObjectId(1));
7667 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
7668 let sketch_object = &scene_delta.new_graph.objects[1];
7669 assert_eq!(sketch_object.id, ObjectId(1));
7670 assert_eq!(
7671 sketch_object.kind,
7672 ObjectKind::Sketch(Sketch {
7673 args: SketchCtor {
7674 on: Plane::Default(PlaneName::Xy)
7675 },
7676 plane: ObjectId(0),
7677 segments: vec![],
7678 constraints: vec![],
7679 })
7680 );
7681 assert_eq!(scene_delta.new_graph.objects.len(), 2);
7682
7683 let point_ctor = PointCtor {
7684 position: Point2d {
7685 x: Expr::Number(Number {
7686 value: 1.0,
7687 units: NumericSuffix::Inch,
7688 }),
7689 y: Expr::Number(Number {
7690 value: 2.0,
7691 units: NumericSuffix::Inch,
7692 }),
7693 },
7694 };
7695 let segment = SegmentCtor::Point(point_ctor);
7696 let (src_delta, scene_delta) = frontend
7697 .add_segment(&mock_ctx, version, sketch_id, segment, None)
7698 .await
7699 .unwrap();
7700 insta::assert_snapshot!("test_new_sketch_add_point_edit_point_1", src_delta.text.as_str());
7701 assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
7702 assert_eq!(scene_delta.new_graph.objects.len(), 3);
7703 for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
7704 assert_eq!(scene_object.id.0, i);
7705 }
7706
7707 let point_id = *scene_delta.new_objects.last().unwrap();
7708
7709 let point_ctor = PointCtor {
7710 position: Point2d {
7711 x: Expr::Number(Number {
7712 value: 3.0,
7713 units: NumericSuffix::Inch,
7714 }),
7715 y: Expr::Number(Number {
7716 value: 4.0,
7717 units: NumericSuffix::Inch,
7718 }),
7719 },
7720 };
7721 let segments = vec![ExistingSegmentCtor {
7722 id: point_id,
7723 ctor: SegmentCtor::Point(point_ctor),
7724 }];
7725 let (src_delta, scene_delta) = frontend
7726 .edit_segments(&mock_ctx, version, sketch_id, segments)
7727 .await
7728 .unwrap();
7729 insta::assert_snapshot!("test_new_sketch_add_point_edit_point_2", src_delta.text.as_str());
7730 assert_eq!(scene_delta.new_objects, vec![]);
7731 assert_eq!(scene_delta.new_graph.objects.len(), 3);
7732
7733 ctx.close().await;
7734 mock_ctx.close().await;
7735 }
7736
7737 #[tokio::test(flavor = "multi_thread")]
7738 async fn test_new_sketch_add_line_edit_line() {
7739 let program = Program::empty();
7740
7741 let mut frontend = FrontendState::new();
7742 frontend.program = program;
7743
7744 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7745 let mock_ctx = ExecutorContext::new_mock(None).await;
7746 let version = Version(0);
7747
7748 let sketch_args = SketchCtor {
7749 on: Plane::Default(PlaneName::Xy),
7750 };
7751 let (_src_delta, scene_delta, sketch_id) = frontend
7752 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
7753 .await
7754 .unwrap();
7755 assert_eq!(sketch_id, ObjectId(1));
7756 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
7757 let sketch_object = &scene_delta.new_graph.objects[1];
7758 assert_eq!(sketch_object.id, ObjectId(1));
7759 assert_eq!(
7760 sketch_object.kind,
7761 ObjectKind::Sketch(Sketch {
7762 args: SketchCtor {
7763 on: Plane::Default(PlaneName::Xy)
7764 },
7765 plane: ObjectId(0),
7766 segments: vec![],
7767 constraints: vec![],
7768 })
7769 );
7770 assert_eq!(scene_delta.new_graph.objects.len(), 2);
7771
7772 let line_ctor = LineCtor {
7773 start: Point2d {
7774 x: Expr::Number(Number {
7775 value: 0.0,
7776 units: NumericSuffix::Mm,
7777 }),
7778 y: Expr::Number(Number {
7779 value: 0.0,
7780 units: NumericSuffix::Mm,
7781 }),
7782 },
7783 end: Point2d {
7784 x: Expr::Number(Number {
7785 value: 10.0,
7786 units: NumericSuffix::Mm,
7787 }),
7788 y: Expr::Number(Number {
7789 value: 10.0,
7790 units: NumericSuffix::Mm,
7791 }),
7792 },
7793 construction: None,
7794 };
7795 let segment = SegmentCtor::Line(line_ctor);
7796 let (src_delta, scene_delta) = frontend
7797 .add_segment(&mock_ctx, version, sketch_id, segment, None)
7798 .await
7799 .unwrap();
7800 insta::assert_snapshot!("test_new_sketch_add_line_edit_line_1", src_delta.text.as_str());
7801 assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
7802 assert_eq!(scene_delta.new_graph.objects.len(), 5);
7803 for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
7804 assert_eq!(scene_object.id.0, i);
7805 }
7806
7807 let line = *scene_delta.new_objects.last().unwrap();
7809
7810 let line_ctor = LineCtor {
7811 start: Point2d {
7812 x: Expr::Number(Number {
7813 value: 1.0,
7814 units: NumericSuffix::Mm,
7815 }),
7816 y: Expr::Number(Number {
7817 value: 2.0,
7818 units: NumericSuffix::Mm,
7819 }),
7820 },
7821 end: Point2d {
7822 x: Expr::Number(Number {
7823 value: 13.0,
7824 units: NumericSuffix::Mm,
7825 }),
7826 y: Expr::Number(Number {
7827 value: 14.0,
7828 units: NumericSuffix::Mm,
7829 }),
7830 },
7831 construction: None,
7832 };
7833 let segments = vec![ExistingSegmentCtor {
7834 id: line,
7835 ctor: SegmentCtor::Line(line_ctor),
7836 }];
7837 let (src_delta, scene_delta) = frontend
7838 .edit_segments(&mock_ctx, version, sketch_id, segments)
7839 .await
7840 .unwrap();
7841 insta::assert_snapshot!("test_new_sketch_add_line_edit_line_2", src_delta.text.as_str());
7842 assert_eq!(scene_delta.new_objects, vec![]);
7843 assert_eq!(scene_delta.new_graph.objects.len(), 5);
7844
7845 ctx.close().await;
7846 mock_ctx.close().await;
7847 }
7848
7849 #[tokio::test(flavor = "multi_thread")]
7850 async fn test_new_sketch_add_arc_edit_arc() {
7851 let program = Program::empty();
7852
7853 let mut frontend = FrontendState::new();
7854 frontend.program = program;
7855
7856 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7857 let mock_ctx = ExecutorContext::new_mock(None).await;
7858 let version = Version(0);
7859
7860 let sketch_args = SketchCtor {
7861 on: Plane::Default(PlaneName::Xy),
7862 };
7863 let (_src_delta, scene_delta, sketch_id) = frontend
7864 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
7865 .await
7866 .unwrap();
7867 assert_eq!(sketch_id, ObjectId(1));
7868 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
7869 let sketch_object = &scene_delta.new_graph.objects[1];
7870 assert_eq!(sketch_object.id, ObjectId(1));
7871 assert_eq!(
7872 sketch_object.kind,
7873 ObjectKind::Sketch(Sketch {
7874 args: SketchCtor {
7875 on: Plane::Default(PlaneName::Xy),
7876 },
7877 plane: ObjectId(0),
7878 segments: vec![],
7879 constraints: vec![],
7880 })
7881 );
7882 assert_eq!(scene_delta.new_graph.objects.len(), 2);
7883
7884 let arc_ctor = ArcCtor {
7885 start: Point2d {
7886 x: Expr::Var(Number {
7887 value: 0.0,
7888 units: NumericSuffix::Mm,
7889 }),
7890 y: Expr::Var(Number {
7891 value: 0.0,
7892 units: NumericSuffix::Mm,
7893 }),
7894 },
7895 end: Point2d {
7896 x: Expr::Var(Number {
7897 value: 10.0,
7898 units: NumericSuffix::Mm,
7899 }),
7900 y: Expr::Var(Number {
7901 value: 10.0,
7902 units: NumericSuffix::Mm,
7903 }),
7904 },
7905 center: Point2d {
7906 x: Expr::Var(Number {
7907 value: 10.0,
7908 units: NumericSuffix::Mm,
7909 }),
7910 y: Expr::Var(Number {
7911 value: 0.0,
7912 units: NumericSuffix::Mm,
7913 }),
7914 },
7915 construction: None,
7916 };
7917 let segment = SegmentCtor::Arc(arc_ctor);
7918 let (src_delta, scene_delta) = frontend
7919 .add_segment(&mock_ctx, version, sketch_id, segment, None)
7920 .await
7921 .unwrap();
7922 insta::assert_snapshot!("test_new_sketch_add_arc_edit_arc_1", src_delta.text.as_str());
7923 assert_eq!(
7924 scene_delta.new_objects,
7925 vec![ObjectId(2), ObjectId(3), ObjectId(4), ObjectId(5)]
7926 );
7927 for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
7928 assert_eq!(scene_object.id.0, i);
7929 }
7930 assert_eq!(scene_delta.new_graph.objects.len(), 6);
7931
7932 let arc = *scene_delta.new_objects.last().unwrap();
7934
7935 let arc_ctor = ArcCtor {
7936 start: Point2d {
7937 x: Expr::Var(Number {
7938 value: 1.0,
7939 units: NumericSuffix::Mm,
7940 }),
7941 y: Expr::Var(Number {
7942 value: 2.0,
7943 units: NumericSuffix::Mm,
7944 }),
7945 },
7946 end: Point2d {
7947 x: Expr::Var(Number {
7948 value: 13.0,
7949 units: NumericSuffix::Mm,
7950 }),
7951 y: Expr::Var(Number {
7952 value: 14.0,
7953 units: NumericSuffix::Mm,
7954 }),
7955 },
7956 center: Point2d {
7957 x: Expr::Var(Number {
7958 value: 13.0,
7959 units: NumericSuffix::Mm,
7960 }),
7961 y: Expr::Var(Number {
7962 value: 2.0,
7963 units: NumericSuffix::Mm,
7964 }),
7965 },
7966 construction: None,
7967 };
7968 let segments = vec![ExistingSegmentCtor {
7969 id: arc,
7970 ctor: SegmentCtor::Arc(arc_ctor),
7971 }];
7972 let (src_delta, scene_delta) = frontend
7973 .edit_segments(&mock_ctx, version, sketch_id, segments)
7974 .await
7975 .unwrap();
7976 insta::assert_snapshot!("test_new_sketch_add_arc_edit_arc_2", src_delta.text.as_str());
7977 assert_eq!(scene_delta.new_objects, vec![]);
7978 assert_eq!(scene_delta.new_graph.objects.len(), 6);
7979
7980 ctx.close().await;
7981 mock_ctx.close().await;
7982 }
7983
7984 #[tokio::test(flavor = "multi_thread")]
7985 async fn test_new_sketch_add_circle_edit_circle() {
7986 let program = Program::empty();
7987
7988 let mut frontend = FrontendState::new();
7989 frontend.program = program;
7990
7991 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7992 let mock_ctx = ExecutorContext::new_mock(None).await;
7993 let version = Version(0);
7994
7995 let sketch_args = SketchCtor {
7996 on: Plane::Default(PlaneName::Xy),
7997 };
7998 let (_src_delta, _scene_delta, sketch_id) = frontend
7999 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
8000 .await
8001 .unwrap();
8002
8003 let circle_ctor = CircleCtor {
8005 start: Point2d {
8006 x: Expr::Var(Number {
8007 value: 5.0,
8008 units: NumericSuffix::Mm,
8009 }),
8010 y: Expr::Var(Number {
8011 value: 0.0,
8012 units: NumericSuffix::Mm,
8013 }),
8014 },
8015 center: Point2d {
8016 x: Expr::Var(Number {
8017 value: 0.0,
8018 units: NumericSuffix::Mm,
8019 }),
8020 y: Expr::Var(Number {
8021 value: 0.0,
8022 units: NumericSuffix::Mm,
8023 }),
8024 },
8025 construction: None,
8026 };
8027 let segment = SegmentCtor::Circle(circle_ctor);
8028 let (src_delta, scene_delta) = frontend
8029 .add_segment(&mock_ctx, version, sketch_id, segment, None)
8030 .await
8031 .unwrap();
8032 insta::assert_snapshot!("test_new_sketch_add_circle_edit_circle_1", src_delta.text.as_str());
8033 assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
8035 assert_eq!(scene_delta.new_graph.objects.len(), 5);
8036
8037 let circle = *scene_delta.new_objects.last().unwrap();
8038
8039 let circle_ctor = CircleCtor {
8041 start: Point2d {
8042 x: Expr::Var(Number {
8043 value: 10.0,
8044 units: NumericSuffix::Mm,
8045 }),
8046 y: Expr::Var(Number {
8047 value: 0.0,
8048 units: NumericSuffix::Mm,
8049 }),
8050 },
8051 center: Point2d {
8052 x: Expr::Var(Number {
8053 value: 3.0,
8054 units: NumericSuffix::Mm,
8055 }),
8056 y: Expr::Var(Number {
8057 value: 4.0,
8058 units: NumericSuffix::Mm,
8059 }),
8060 },
8061 construction: None,
8062 };
8063 let segments = vec![ExistingSegmentCtor {
8064 id: circle,
8065 ctor: SegmentCtor::Circle(circle_ctor),
8066 }];
8067 let (src_delta, scene_delta) = frontend
8068 .edit_segments(&mock_ctx, version, sketch_id, segments)
8069 .await
8070 .unwrap();
8071 insta::assert_snapshot!("test_new_sketch_add_circle_edit_circle_2", src_delta.text.as_str());
8072 assert_eq!(scene_delta.new_objects, vec![]);
8073 assert_eq!(scene_delta.new_graph.objects.len(), 5);
8074
8075 ctx.close().await;
8076 mock_ctx.close().await;
8077 }
8078
8079 #[tokio::test(flavor = "multi_thread")]
8080 async fn test_delete_circle() {
8081 let initial_source = "sketch001 = sketch(on = XY) {
8082 circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
8083}
8084";
8085
8086 let program = Program::parse(initial_source).unwrap().0.unwrap();
8087 let mut frontend = FrontendState::new();
8088
8089 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8090 let mock_ctx = ExecutorContext::new_mock(None).await;
8091 let version = Version(0);
8092
8093 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8094 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8095 let sketch_id = sketch_object.id;
8096 let sketch = expect_sketch(sketch_object);
8097
8098 assert_eq!(sketch.segments.len(), 3);
8100 let circle_id = sketch.segments[2];
8101
8102 let (src_delta, scene_delta) = frontend
8104 .delete_objects(&mock_ctx, version, sketch_id, vec![], vec![circle_id])
8105 .await
8106 .unwrap();
8107 insta::assert_snapshot!("test_delete_circle", src_delta.text.as_str());
8108 let new_sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8109 let new_sketch = expect_sketch(new_sketch_object);
8110 assert_eq!(new_sketch.segments.len(), 0);
8111
8112 ctx.close().await;
8113 mock_ctx.close().await;
8114 }
8115
8116 #[tokio::test(flavor = "multi_thread")]
8117 async fn test_edit_circle_via_point() {
8118 let initial_source = "sketch001 = sketch(on = XY) {
8119 circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
8120}
8121";
8122
8123 let program = Program::parse(initial_source).unwrap().0.unwrap();
8124 let mut frontend = FrontendState::new();
8125
8126 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8127 let mock_ctx = ExecutorContext::new_mock(None).await;
8128 let version = Version(0);
8129
8130 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8131 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8132 let sketch_id = sketch_object.id;
8133 let sketch = expect_sketch(sketch_object);
8134
8135 let circle_id = sketch
8137 .segments
8138 .iter()
8139 .copied()
8140 .find(|seg_id| {
8141 matches!(
8142 &frontend.scene_graph.objects[seg_id.0].kind,
8143 ObjectKind::Segment {
8144 segment: Segment::Circle(_)
8145 }
8146 )
8147 })
8148 .expect("Expected a circle segment in sketch");
8149 let circle_object = &frontend.scene_graph.objects[circle_id.0];
8150 let ObjectKind::Segment {
8151 segment: Segment::Circle(circle),
8152 } = &circle_object.kind
8153 else {
8154 panic!("Expected circle segment, got: {:?}", circle_object.kind);
8155 };
8156 let start_point_id = circle.start;
8157
8158 let segments = vec![ExistingSegmentCtor {
8160 id: start_point_id,
8161 ctor: SegmentCtor::Point(PointCtor {
8162 position: Point2d {
8163 x: Expr::Var(Number {
8164 value: 7.0,
8165 units: NumericSuffix::Mm,
8166 }),
8167 y: Expr::Var(Number {
8168 value: 1.0,
8169 units: NumericSuffix::Mm,
8170 }),
8171 },
8172 }),
8173 }];
8174 let (src_delta, _scene_delta) = frontend
8175 .edit_segments(&mock_ctx, version, sketch_id, segments)
8176 .await
8177 .unwrap();
8178 insta::assert_snapshot!("test_edit_circle_via_point", src_delta.text.as_str());
8179
8180 ctx.close().await;
8181 mock_ctx.close().await;
8182 }
8183
8184 #[tokio::test(flavor = "multi_thread")]
8185 async fn test_add_line_when_sketch_block_uses_variable() {
8186 let initial_source = "s = sketch(on = XY) {}
8187";
8188
8189 let program = Program::parse(initial_source).unwrap().0.unwrap();
8190
8191 let mut frontend = FrontendState::new();
8192
8193 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8194 let mock_ctx = ExecutorContext::new_mock(None).await;
8195 let version = Version(0);
8196
8197 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8198 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8199 let sketch_id = sketch_object.id;
8200
8201 let line_ctor = LineCtor {
8202 start: Point2d {
8203 x: Expr::Number(Number {
8204 value: 0.0,
8205 units: NumericSuffix::Mm,
8206 }),
8207 y: Expr::Number(Number {
8208 value: 0.0,
8209 units: NumericSuffix::Mm,
8210 }),
8211 },
8212 end: Point2d {
8213 x: Expr::Number(Number {
8214 value: 10.0,
8215 units: NumericSuffix::Mm,
8216 }),
8217 y: Expr::Number(Number {
8218 value: 10.0,
8219 units: NumericSuffix::Mm,
8220 }),
8221 },
8222 construction: None,
8223 };
8224 let segment = SegmentCtor::Line(line_ctor);
8225 let (src_delta, scene_delta) = frontend
8226 .add_segment(&mock_ctx, version, sketch_id, segment, None)
8227 .await
8228 .unwrap();
8229 insta::assert_snapshot!("test_add_line_when_sketch_block_uses_variable", src_delta.text.as_str());
8230 assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
8231 assert_eq!(scene_delta.new_graph.objects.len(), 5);
8232
8233 ctx.close().await;
8234 mock_ctx.close().await;
8235 }
8236
8237 #[tokio::test(flavor = "multi_thread")]
8238 async fn test_new_sketch_add_line_delete_sketch() {
8239 let program = Program::empty();
8240
8241 let mut frontend = FrontendState::new();
8242 frontend.program = program;
8243
8244 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8245 let mock_ctx = ExecutorContext::new_mock(None).await;
8246 let version = Version(0);
8247
8248 let sketch_args = SketchCtor {
8249 on: Plane::Default(PlaneName::Xy),
8250 };
8251 let (_src_delta, scene_delta, sketch_id) = frontend
8252 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
8253 .await
8254 .unwrap();
8255 assert_eq!(sketch_id, ObjectId(1));
8256 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
8257 let sketch_object = &scene_delta.new_graph.objects[1];
8258 assert_eq!(sketch_object.id, ObjectId(1));
8259 assert_eq!(
8260 sketch_object.kind,
8261 ObjectKind::Sketch(Sketch {
8262 args: SketchCtor {
8263 on: Plane::Default(PlaneName::Xy)
8264 },
8265 plane: ObjectId(0),
8266 segments: vec![],
8267 constraints: vec![],
8268 })
8269 );
8270 assert_eq!(scene_delta.new_graph.objects.len(), 2);
8271
8272 let line_ctor = LineCtor {
8273 start: Point2d {
8274 x: Expr::Number(Number {
8275 value: 0.0,
8276 units: NumericSuffix::Mm,
8277 }),
8278 y: Expr::Number(Number {
8279 value: 0.0,
8280 units: NumericSuffix::Mm,
8281 }),
8282 },
8283 end: Point2d {
8284 x: Expr::Number(Number {
8285 value: 10.0,
8286 units: NumericSuffix::Mm,
8287 }),
8288 y: Expr::Number(Number {
8289 value: 10.0,
8290 units: NumericSuffix::Mm,
8291 }),
8292 },
8293 construction: None,
8294 };
8295 let segment = SegmentCtor::Line(line_ctor);
8296 let (src_delta, scene_delta) = frontend
8297 .add_segment(&mock_ctx, version, sketch_id, segment, None)
8298 .await
8299 .unwrap();
8300 insta::assert_snapshot!("test_new_sketch_add_line_delete_sketch_1", src_delta.text.as_str());
8301 assert_eq!(scene_delta.new_graph.objects.len(), 5);
8302
8303 let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8304 insta::assert_snapshot!("test_new_sketch_add_line_delete_sketch_2", src_delta.text.as_str());
8305 assert_eq!(scene_delta.new_graph.objects.len(), 0);
8306
8307 ctx.close().await;
8308 mock_ctx.close().await;
8309 }
8310
8311 #[tokio::test(flavor = "multi_thread")]
8312 async fn test_delete_sketch_when_sketch_block_uses_variable() {
8313 let initial_source = "s = sketch(on = XY) {}
8314";
8315
8316 let program = Program::parse(initial_source).unwrap().0.unwrap();
8317
8318 let mut frontend = FrontendState::new();
8319
8320 let ctx = ExecutorContext::new_with_engine(sync::Arc::new(EngineManager::new_mock()), Default::default());
8321 let version = Version(0);
8322
8323 frontend.hack_set_program(&ctx, program).await.unwrap();
8324 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8325 let sketch_id = sketch_object.id;
8326
8327 let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8328 insta::assert_snapshot!(
8329 "test_delete_sketch_when_sketch_block_uses_variable",
8330 src_delta.text.as_str()
8331 );
8332 assert_eq!(scene_delta.new_graph.objects.len(), 0);
8333
8334 ctx.close().await;
8335 }
8336
8337 #[tokio::test(flavor = "multi_thread")]
8338 async fn test_delete_sketch_after_comment() {
8339 let initial_source = "sketch001 = sketch(on = XZ) {
8340}
8341";
8342
8343 let program = Program::parse(initial_source).unwrap().0.unwrap();
8344 let mut frontend = FrontendState::new();
8345
8346 let ctx = ExecutorContext::new_with_engine(sync::Arc::new(EngineManager::new_mock()), Default::default());
8347 let version = Version(0);
8348
8349 frontend.hack_set_program(&ctx, program).await.unwrap();
8350 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8351 let sketch_id = sketch_object.id;
8352 let original_source = sketch_object.source.clone();
8353
8354 let commented_source = "// test 1
8355sketch001 = sketch(on = XZ) {
8356}
8357";
8358 let commented_program = Program::parse(commented_source).unwrap().0.unwrap();
8359 frontend.engine_execute(&ctx, commented_program).await.unwrap();
8360
8361 let cached_sketch_object = &frontend.scene_graph.objects[sketch_id.0];
8362 assert_eq!(cached_sketch_object.source, original_source);
8363
8364 let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8365 assert!(
8366 !src_delta.text.contains("sketch001"),
8367 "sketch was not deleted: {}",
8368 src_delta.text
8369 );
8370 insta::assert_snapshot!("test_delete_sketch_after_comment", src_delta.text.as_str());
8372 assert_eq!(scene_delta.new_graph.objects.len(), 0);
8373
8374 ctx.close().await;
8375 }
8376
8377 #[tokio::test(flavor = "multi_thread")]
8378 async fn test_delete_sketch_preserves_pre_comment_when_followed_by_code() {
8379 let initial_source = "sketch001 = sketch(on = XZ) {
8380}
8381foo = 1
8382";
8383
8384 let program = Program::parse(initial_source).unwrap().0.unwrap();
8385 let mut frontend = FrontendState::new();
8386
8387 let ctx = ExecutorContext::new_with_engine(sync::Arc::new(EngineManager::new_mock()), Default::default());
8388 let version = Version(0);
8389
8390 frontend.hack_set_program(&ctx, program).await.unwrap();
8391 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8392 let sketch_id = sketch_object.id;
8393
8394 let commented_source = "// keep me
8395sketch001 = sketch(on = XZ) {
8396}
8397foo = 1
8398";
8399 let commented_program = Program::parse(commented_source).unwrap().0.unwrap();
8400 frontend.engine_execute(&ctx, commented_program).await.unwrap();
8401
8402 let (src_delta, _scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8403 insta::assert_snapshot!(
8405 "test_delete_sketch_preserves_pre_comment_when_followed_by_code",
8406 src_delta.text.as_str()
8407 );
8408
8409 ctx.close().await;
8410 }
8411
8412 #[tokio::test(flavor = "multi_thread")]
8413 async fn test_delete_segment_preserves_pre_comment() {
8414 let initial_source = "\
8415sketch(on = XY) {
8416 point(at = [var 1, var 2])
8417 // describe the middle point
8418 point(at = [var 3, var 4])
8419 point(at = [var 5, var 6])
8420}
8421";
8422
8423 let program = Program::parse(initial_source).unwrap().0.unwrap();
8424 let mut frontend = FrontendState::new();
8425
8426 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8427 let mock_ctx = ExecutorContext::new_mock(None).await;
8428 let version = Version(0);
8429
8430 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8431 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8432 let sketch_id = sketch_object.id;
8433 let sketch = expect_sketch(sketch_object);
8434
8435 let middle_point_id = *sketch.segments.get(1).unwrap();
8436
8437 let (src_delta, _scene_delta) = frontend
8438 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![middle_point_id])
8439 .await
8440 .unwrap();
8441 insta::assert_snapshot!("test_delete_segment_preserves_pre_comment", src_delta.text.as_str());
8444
8445 ctx.close().await;
8446 mock_ctx.close().await;
8447 }
8448
8449 #[tokio::test(flavor = "multi_thread")]
8450 async fn test_delete_last_segment_preserves_pre_comment() {
8451 let initial_source = "\
8452sketch(on = XY) {
8453 point(at = [var 1, var 2])
8454 // describe the trailing point
8455 point(at = [var 3, var 4])
8456}
8457";
8458
8459 let program = Program::parse(initial_source).unwrap().0.unwrap();
8460 let mut frontend = FrontendState::new();
8461
8462 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8463 let mock_ctx = ExecutorContext::new_mock(None).await;
8464 let version = Version(0);
8465
8466 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8467 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8468 let sketch_id = sketch_object.id;
8469 let sketch = expect_sketch(sketch_object);
8470
8471 let last_point_id = *sketch.segments.last().unwrap();
8472
8473 let (src_delta, _scene_delta) = frontend
8474 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![last_point_id])
8475 .await
8476 .unwrap();
8477 insta::assert_snapshot!(
8480 "test_delete_last_segment_preserves_pre_comment",
8481 src_delta.text.as_str()
8482 );
8483
8484 ctx.close().await;
8485 mock_ctx.close().await;
8486 }
8487
8488 #[tokio::test(flavor = "multi_thread")]
8489 async fn test_delete_segment_drops_inline_trailing_comment() {
8490 let initial_source = "\
8491sketch(on = XY) {
8492 point(at = [var 1, var 2])
8493 point(at = [var 3, var 4]) // same-line note that gets dropped
8494 point(at = [var 5, var 6])
8495}
8496";
8497
8498 let program = Program::parse(initial_source).unwrap().0.unwrap();
8499 let mut frontend = FrontendState::new();
8500
8501 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8502 let mock_ctx = ExecutorContext::new_mock(None).await;
8503 let version = Version(0);
8504
8505 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8506 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8507 let sketch_id = sketch_object.id;
8508 let sketch = expect_sketch(sketch_object);
8509
8510 let middle_point_id = *sketch.segments.get(1).unwrap();
8511
8512 let (src_delta, _scene_delta) = frontend
8513 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![middle_point_id])
8514 .await
8515 .unwrap();
8516 assert!(
8518 !src_delta.text.contains("same-line note"),
8519 "inline comment should have been removed: {}",
8520 src_delta.text
8521 );
8522
8523 ctx.close().await;
8524 mock_ctx.close().await;
8525 }
8526
8527 #[tokio::test(flavor = "multi_thread")]
8528 async fn test_delete_segments_preserves_block_comments_across_positions() {
8529 let initial_source = "\
8537sketch(on = XY) {
8538 /* above first - moves to middle */
8539 point(at = [var 1, var 2]) /* same-line on first - dropped */
8540 /* above middle - stays */
8541 point(at = [var 3, var 4])
8542 /* above last - moves to trailing meta */
8543 point(at = [var 5, var 6])
8544}
8545";
8546
8547 let program = Program::parse(initial_source).unwrap().0.unwrap();
8548 let mut frontend = FrontendState::new();
8549
8550 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8551 let mock_ctx = ExecutorContext::new_mock(None).await;
8552 let version = Version(0);
8553
8554 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8555 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8556 let sketch_id = sketch_object.id;
8557 let sketch = expect_sketch(sketch_object);
8558
8559 let first_point_id = *sketch.segments.first().unwrap();
8560 let last_point_id = *sketch.segments.last().unwrap();
8561
8562 let (src_delta, _scene_delta) = frontend
8563 .delete_objects(
8564 &mock_ctx,
8565 version,
8566 sketch_id,
8567 Vec::new(),
8568 vec![first_point_id, last_point_id],
8569 )
8570 .await
8571 .unwrap();
8572 insta::assert_snapshot!(
8573 "test_delete_segments_preserves_block_comments_across_positions",
8574 src_delta.text.as_str()
8575 );
8576
8577 ctx.close().await;
8578 mock_ctx.close().await;
8579 }
8580
8581 #[tokio::test(flavor = "multi_thread")]
8582 async fn test_edit_line_when_editing_its_start_point() {
8583 let initial_source = "\
8584sketch(on = XY) {
8585 line(start = [var 1, var 2], end = [var 3, var 4])
8586}
8587";
8588
8589 let program = Program::parse(initial_source).unwrap().0.unwrap();
8590
8591 let mut frontend = FrontendState::new();
8592
8593 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8594 let mock_ctx = ExecutorContext::new_mock(None).await;
8595 let version = Version(0);
8596
8597 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8598 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8599 let sketch_id = sketch_object.id;
8600 let sketch = expect_sketch(sketch_object);
8601
8602 let point_id = *sketch.segments.first().unwrap();
8603
8604 let point_ctor = PointCtor {
8605 position: Point2d {
8606 x: Expr::Var(Number {
8607 value: 5.0,
8608 units: NumericSuffix::Inch,
8609 }),
8610 y: Expr::Var(Number {
8611 value: 6.0,
8612 units: NumericSuffix::Inch,
8613 }),
8614 },
8615 };
8616 let segments = vec![ExistingSegmentCtor {
8617 id: point_id,
8618 ctor: SegmentCtor::Point(point_ctor),
8619 }];
8620 let (src_delta, scene_delta) = frontend
8621 .edit_segments(&mock_ctx, version, sketch_id, segments)
8622 .await
8623 .unwrap();
8624 insta::assert_snapshot!("test_edit_line_when_editing_its_start_point", src_delta.text.as_str());
8625 assert_eq!(scene_delta.new_objects, vec![]);
8626 assert_eq!(scene_delta.new_graph.objects.len(), 5);
8627
8628 ctx.close().await;
8629 mock_ctx.close().await;
8630 }
8631
8632 #[tokio::test(flavor = "multi_thread")]
8633 async fn test_edit_line_when_editing_its_end_point() {
8634 let initial_source = "\
8635sketch(on = XY) {
8636 line(start = [var 1, var 2], end = [var 3, var 4])
8637}
8638";
8639
8640 let program = Program::parse(initial_source).unwrap().0.unwrap();
8641
8642 let mut frontend = FrontendState::new();
8643
8644 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8645 let mock_ctx = ExecutorContext::new_mock(None).await;
8646 let version = Version(0);
8647
8648 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8649 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8650 let sketch_id = sketch_object.id;
8651 let sketch = expect_sketch(sketch_object);
8652 let point_id = *sketch.segments.get(1).unwrap();
8653
8654 let point_ctor = PointCtor {
8655 position: Point2d {
8656 x: Expr::Var(Number {
8657 value: 5.0,
8658 units: NumericSuffix::Inch,
8659 }),
8660 y: Expr::Var(Number {
8661 value: 6.0,
8662 units: NumericSuffix::Inch,
8663 }),
8664 },
8665 };
8666 let segments = vec![ExistingSegmentCtor {
8667 id: point_id,
8668 ctor: SegmentCtor::Point(point_ctor),
8669 }];
8670 let (src_delta, scene_delta) = frontend
8671 .edit_segments(&mock_ctx, version, sketch_id, segments)
8672 .await
8673 .unwrap();
8674 insta::assert_snapshot!("test_edit_line_when_editing_its_end_point", src_delta.text.as_str());
8675 assert_eq!(scene_delta.new_objects, vec![]);
8676 assert_eq!(
8677 scene_delta.new_graph.objects.len(),
8678 5,
8679 "{:#?}",
8680 scene_delta.new_graph.objects
8681 );
8682
8683 ctx.close().await;
8684 mock_ctx.close().await;
8685 }
8686
8687 #[tokio::test(flavor = "multi_thread")]
8688 async fn test_edit_line_with_coincident_feedback() {
8689 let initial_source = "\
8690sketch(on = XY) {
8691 line1 = line(start = [var 1, var 2], end = [var 1, var 2])
8692 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8693 fixed([line1.start, [0, 0]])
8694 coincident([line1.end, line2.start])
8695 equalLength([line1, line2])
8696}
8697";
8698
8699 let program = Program::parse(initial_source).unwrap().0.unwrap();
8700
8701 let mut frontend = FrontendState::new();
8702
8703 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8704 let mock_ctx = ExecutorContext::new_mock(None).await;
8705 let version = Version(0);
8706
8707 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8708 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8709 let sketch_id = sketch_object.id;
8710 let sketch = expect_sketch(sketch_object);
8711 let line2_end_id = *sketch.segments.get(4).unwrap();
8712
8713 let segments = vec![ExistingSegmentCtor {
8714 id: line2_end_id,
8715 ctor: SegmentCtor::Point(PointCtor {
8716 position: Point2d {
8717 x: Expr::Var(Number {
8718 value: 9.0,
8719 units: NumericSuffix::None,
8720 }),
8721 y: Expr::Var(Number {
8722 value: 10.0,
8723 units: NumericSuffix::None,
8724 }),
8725 },
8726 }),
8727 }];
8728 let (src_delta, scene_delta) = frontend
8729 .edit_segments(&mock_ctx, version, sketch_id, segments)
8730 .await
8731 .unwrap();
8732 insta::assert_snapshot!("test_edit_line_with_coincident_feedback", src_delta.text.as_str());
8733 assert_eq!(
8734 scene_delta.new_graph.objects.len(),
8735 11,
8736 "{:#?}",
8737 scene_delta.new_graph.objects
8738 );
8739
8740 ctx.close().await;
8741 mock_ctx.close().await;
8742 }
8743
8744 #[tokio::test(flavor = "multi_thread")]
8745 async fn test_edit_segments_persists_solver_feedback_for_next_mock_execute() {
8746 let initial_source = "\
8747sketch(on = XY) {
8748 line1 = line(start = [var 1, var 2], end = [var 1, var 2])
8749 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8750 fixed([line1.start, [0, 0]])
8751 coincident([line1.end, line2.start])
8752 equalLength([line1, line2])
8753}
8754";
8755
8756 let program = Program::parse(initial_source).unwrap().0.unwrap();
8757 let mut frontend = FrontendState::new();
8758 let mock_ctx = ExecutorContext::new_mock(None).await;
8759 let version = Version(0);
8760
8761 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8762 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8763 let sketch_id = sketch_object.id;
8764 let sketch = expect_sketch(sketch_object);
8765 let line2_end_id = *sketch.segments.get(4).unwrap();
8766
8767 let segments = vec![ExistingSegmentCtor {
8768 id: line2_end_id,
8769 ctor: SegmentCtor::Point(PointCtor {
8770 position: Point2d {
8771 x: Expr::Var(Number {
8772 value: 9.0,
8773 units: NumericSuffix::None,
8774 }),
8775 y: Expr::Var(Number {
8776 value: 10.0,
8777 units: NumericSuffix::None,
8778 }),
8779 },
8780 }),
8781 }];
8782 let (edited_source, _) = frontend
8783 .edit_segments(&mock_ctx, version, sketch_id, segments)
8784 .await
8785 .unwrap();
8786
8787 let (mock_source, _) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
8788 assert_eq!(mock_source.text, edited_source.text);
8789
8790 mock_ctx.close().await;
8791 }
8792
8793 #[tokio::test(flavor = "multi_thread")]
8796 async fn test_preview_edit_segments_does_not_persist_solver_feedback() {
8797 let initial_source = "\
8798sketch(on = XY) {
8799 line1 = line(start = [var 1, var 2], end = [var 1, var 2])
8800 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8801 fixed([line1.start, [0, 0]])
8802 coincident([line1.end, line2.start])
8803 equalLength([line1, line2])
8804}
8805";
8806
8807 let program = Program::parse(initial_source).unwrap().0.unwrap();
8808 let mut frontend = FrontendState::new();
8809 let mock_ctx = ExecutorContext::new_mock(None).await;
8810 let version = Version(0);
8811
8812 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8813 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8814 let sketch_id = sketch_object.id;
8815 let sketch = expect_sketch(sketch_object);
8816 let line2_end_id = *sketch.segments.get(4).unwrap();
8817
8818 let segments = vec![ExistingSegmentCtor {
8819 id: line2_end_id,
8820 ctor: SegmentCtor::Point(PointCtor {
8821 position: Point2d {
8822 x: Expr::Var(Number {
8823 value: 9.0,
8824 units: NumericSuffix::None,
8825 }),
8826 y: Expr::Var(Number {
8827 value: 10.0,
8828 units: NumericSuffix::None,
8829 }),
8830 },
8831 }),
8832 }];
8833 let (preview_source, preview_delta) = frontend
8834 .edit_segments_with_options(
8835 &mock_ctx,
8836 version,
8837 sketch_id,
8838 segments,
8839 EditSegmentsOptions {
8840 anchor_segment_ids: Some(vec![line2_end_id]),
8841 drag_anchors: Vec::new(),
8842 commit_solved_initial_guesses: false,
8843 },
8844 )
8845 .await
8846 .unwrap();
8847
8848 assert!(
8849 !preview_delta.exec_outcome.var_solutions.is_empty(),
8850 "preview solve should still solve and return geometry feedback"
8851 );
8852 assert!(
8853 preview_source
8854 .text
8855 .contains("line1 = line(start = [var 1, var 2], end = [var 1, var 2])")
8856 );
8857 assert!(
8858 preview_source
8859 .text
8860 .contains("line2 = line(start = [var 5, var 6], end = [var 9, var 10])")
8861 );
8862
8863 let (mock_source, _) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
8864 assert_eq!(mock_source.text, preview_source.text);
8865
8866 mock_ctx.close().await;
8867 }
8868
8869 #[tokio::test(flavor = "multi_thread")]
8870 async fn test_add_constraint_persists_solver_feedback_for_next_mock_execute() {
8871 let initial_source = "\
8872sketch(on = XY) {
8873 line1 = line(start = [var 0, var 0], end = [var 10, var 0])
8874}
8875";
8876
8877 let program = Program::parse(initial_source).unwrap().0.unwrap();
8878 let mut frontend = FrontendState::new();
8879 let mock_ctx = ExecutorContext::new_mock(None).await;
8880 let version = Version(0);
8881
8882 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8883 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8884 let sketch_id = sketch_object.id;
8885 let sketch = expect_sketch(sketch_object);
8886 let line_end_id = *sketch.segments.get(1).unwrap();
8887
8888 let constraint = Constraint::Fixed(Fixed {
8889 points: vec![FixedPoint {
8890 point: line_end_id,
8891 position: Point2d {
8892 x: Number {
8893 value: 20.0,
8894 units: NumericSuffix::Mm,
8895 },
8896 y: Number {
8897 value: 0.0,
8898 units: NumericSuffix::Mm,
8899 },
8900 },
8901 }],
8902 });
8903 let (constraint_source, _) = frontend
8904 .add_constraint(&mock_ctx, version, sketch_id, constraint)
8905 .await
8906 .unwrap();
8907
8908 assert!(
8909 constraint_source
8910 .text
8911 .contains("line1 = line(start = [var 0, var 0], end = [var 20, var 0])"),
8912 "{}",
8913 constraint_source.text
8914 );
8915 let (mock_source, _) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
8916 assert_eq!(mock_source.text, constraint_source.text);
8917
8918 mock_ctx.close().await;
8919 }
8920
8921 #[test]
8922 fn test_no_solver_feedback_preserves_original_source() {
8923 let initial_source = "\
8924@settings(defaultLengthUnit = in, kclVersion = 2.0)
8925cylinder = startSketchOn(XY)
8926 |> circle(center= [0, 0], radius= 22)
8927 |> extrude(length = 14)
8928";
8929 let mut frontend = FrontendState::new();
8930 frontend.program = Program::parse(initial_source).unwrap().0.unwrap();
8931 let outcome = ExecOutcome {
8932 variables: Default::default(),
8933 operations: Default::default(),
8934 artifact_graph: Default::default(),
8935 scene_objects: Default::default(),
8936 source_range_to_object: Default::default(),
8937 var_solutions: Default::default(),
8938 refactor_metadata: Default::default(),
8939 issues: Default::default(),
8940 filenames: Default::default(),
8941 default_planes: Default::default(),
8942 };
8943
8944 let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
8945
8946 assert_eq!(source_delta.text, initial_source);
8947 }
8948
8949 #[tokio::test(flavor = "multi_thread")]
8952 async fn test_edit_segments_with_anchor_ids_limits_drag_fixed_constraints() {
8953 let initial_source = "\
8954sketch(on = XY) {
8955 point1 = point(at = [var 0mm, var 0mm])
8956 point2 = point(at = [var 0mm, var 0mm])
8957 coincident([point1, point2])
8958}
8959";
8960
8961 let program = Program::parse(initial_source).unwrap().0.unwrap();
8962 let mut frontend = FrontendState::new();
8963 let mock_ctx = ExecutorContext::new_mock(None).await;
8964 let version = Version(0);
8965
8966 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8967 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8968 let sketch_id = sketch_object.id;
8969 let sketch = expect_sketch(sketch_object);
8970 let point1_id = sketch.segments[0];
8971 let point2_id = sketch.segments[1];
8972
8973 let segments = vec![
8974 ExistingSegmentCtor {
8975 id: point1_id,
8976 ctor: SegmentCtor::Point(PointCtor {
8977 position: point_expr_mm(10.0, 0.0),
8978 }),
8979 },
8980 ExistingSegmentCtor {
8981 id: point2_id,
8982 ctor: SegmentCtor::Point(PointCtor {
8983 position: point_expr_mm(100.0, 0.0),
8984 }),
8985 },
8986 ];
8987 let (_, scene_delta) = frontend
8988 .edit_segments_with_options(
8989 &mock_ctx,
8990 version,
8991 sketch_id,
8992 segments,
8993 EditSegmentsOptions {
8994 anchor_segment_ids: Some(vec![point1_id]),
8995 drag_anchors: Vec::new(),
8996 commit_solved_initial_guesses: true,
8997 },
8998 )
8999 .await
9000 .unwrap();
9001
9002 assert_point_position_close(
9003 point_position(&scene_delta.new_graph, point1_id),
9004 point_number_mm(10.0, 0.0),
9005 );
9006 assert_point_position_close(
9007 point_position(&scene_delta.new_graph, point2_id),
9008 point_number_mm(10.0, 0.0),
9009 );
9010
9011 mock_ctx.close().await;
9012 }
9013
9014 fn collect_sketch_var_literals_with_value(program: &Program, value: f64) -> Vec<(SourceRange, ast::NodePath)> {
9017 use std::cell::RefCell;
9018 struct Collector {
9019 target: f64,
9020 out: RefCell<Vec<(SourceRange, ast::NodePath)>>,
9021 }
9022 impl<'a> crate::walk::Visitor<'a> for &Collector {
9023 type Error = crate::front::Error;
9024 fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
9025 if let crate::walk::Node::SketchVar(sketch_var) = node
9026 && let (Some(initial), Some(node_path)) = (&sketch_var.initial, &sketch_var.node_path)
9027 && (initial.value - self.target).abs() < 1e-9
9028 {
9029 self.out
9030 .borrow_mut()
9031 .push((SourceRange::from(initial.as_ref()), node_path.clone()));
9032 }
9033 for child in node.children().iter() {
9034 if !child.visit(*self)? {
9035 return Ok(false);
9036 }
9037 }
9038 Ok(true)
9039 }
9040 }
9041 let collector = Collector {
9042 target: value,
9043 out: Default::default(),
9044 };
9045 let _ = crate::walk::Node::from(&program.ast).visit(&collector);
9046 collector.out.into_inner()
9047 }
9048
9049 fn collect_all_sketch_vars(program: &Program) -> Vec<(SourceRange, ast::NodePath)> {
9052 use std::cell::RefCell;
9053 struct Collector {
9054 out: RefCell<Vec<(SourceRange, ast::NodePath)>>,
9055 }
9056 impl<'a> crate::walk::Visitor<'a> for &Collector {
9057 type Error = crate::front::Error;
9058 fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
9059 if let crate::walk::Node::SketchVar(sketch_var) = node
9060 && let Some(node_path) = &sketch_var.node_path
9061 {
9062 self.out
9063 .borrow_mut()
9064 .push((SourceRange::from(sketch_var), node_path.clone()));
9065 }
9066 for child in node.children().iter() {
9067 if !child.visit(*self)? {
9068 return Ok(false);
9069 }
9070 }
9071 Ok(true)
9072 }
9073 }
9074 let collector = Collector {
9075 out: Default::default(),
9076 };
9077 let _ = crate::walk::Node::from(&program.ast).visit(&collector);
9078 collector.out.into_inner()
9079 }
9080
9081 fn empty_exec_outcome_with_var_solutions(
9082 var_solutions: Vec<(SourceRange, Option<ast::NodePath>, Number)>,
9083 ) -> ExecOutcome {
9084 ExecOutcome {
9085 variables: Default::default(),
9086 operations: Default::default(),
9087 artifact_graph: Default::default(),
9088 scene_objects: Default::default(),
9089 source_range_to_object: Default::default(),
9090 var_solutions,
9091 refactor_metadata: Default::default(),
9092 issues: Default::default(),
9093 filenames: Default::default(),
9094 default_planes: Default::default(),
9095 }
9096 }
9097
9098 #[test]
9102 fn test_commit_var_solution_by_node_path_updates_sketch_var() {
9103 let initial_source = "\
9104sketch(on = XY) {
9105 line1 = line(start = [var 0, var 0], end = [var 10mm, var 0])
9106}
9107";
9108 let program = Program::parse(initial_source).unwrap().0.unwrap();
9109 let matches = collect_sketch_var_literals_with_value(&program, 10.0);
9110 assert_eq!(matches.len(), 1, "expected exactly one `var 10mm`");
9111 let (literal_range, node_path) = matches.into_iter().next().unwrap();
9112
9113 let mut frontend = FrontendState::new();
9114 frontend.program = program;
9115
9116 let outcome = empty_exec_outcome_with_var_solutions(vec![(
9117 literal_range,
9118 Some(node_path),
9119 Number {
9120 value: 25.0,
9121 units: NumericSuffix::Mm,
9122 },
9123 )]);
9124
9125 let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9126
9127 insta::assert_snapshot!(
9128 "test_commit_var_solution_by_node_path_updates_sketch_var",
9129 source_delta.text
9130 );
9131 }
9132
9133 #[test]
9140 fn test_commit_var_solution_survives_whitespace_shift_earlier_in_file() {
9141 let compact_source = "\
9142sketch(on = XY) {
9143 line1 = line(start = [var 0, var 0], end = [var 10mm, var 0])
9144}
9145";
9146 let padded_source = "\
9147// added comment\n// added comment\n\nsketch(on = XY) {
9148 line1 = line(start = [var 0, var 0], end = [var 10mm, var 0])
9149}
9150";
9151 let compact_program = Program::parse(compact_source).unwrap().0.unwrap();
9152 let padded_program = Program::parse(padded_source).unwrap().0.unwrap();
9153
9154 let compact_match = collect_sketch_var_literals_with_value(&compact_program, 10.0)
9155 .into_iter()
9156 .next()
9157 .expect("expected `var 10mm` in compact source");
9158 let padded_match = collect_sketch_var_literals_with_value(&padded_program, 10.0)
9159 .into_iter()
9160 .next()
9161 .expect("expected `var 10mm` in padded source");
9162
9163 assert_ne!(
9164 compact_match.0, padded_match.0,
9165 "byte offsets must differ for this test to be meaningful"
9166 );
9167 assert_eq!(
9168 compact_match.1, padded_match.1,
9169 "node paths must agree across whitespace; that's the whole point of NodePath",
9170 );
9171
9172 let mut frontend = FrontendState::new();
9173 frontend.program = padded_program;
9174
9175 let outcome = empty_exec_outcome_with_var_solutions(vec![(
9177 compact_match.0,
9178 Some(compact_match.1),
9179 Number {
9180 value: 30.0,
9181 units: NumericSuffix::Mm,
9182 },
9183 )]);
9184
9185 let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9186
9187 insta::assert_snapshot!(
9188 "test_commit_var_solution_survives_whitespace_shift_earlier_in_file",
9189 source_delta.text
9190 );
9191 }
9192
9193 #[test]
9197 fn test_commit_var_solution_node_path_wins_when_source_range_points_at_wrong_var() {
9198 let initial_source = "\
9199sketch(on = XY) {
9200 line1 = line(start = [var 10mm, var 0mm], end = [var 20mm, var 0mm])
9201}
9202";
9203 let program = Program::parse(initial_source).unwrap().0.unwrap();
9204
9205 let var_10 = collect_sketch_var_literals_with_value(&program, 10.0)
9206 .into_iter()
9207 .next()
9208 .expect("expected `var 10mm`");
9209 let var_20 = collect_sketch_var_literals_with_value(&program, 20.0)
9210 .into_iter()
9211 .next()
9212 .expect("expected `var 20mm`");
9213
9214 let mut frontend = FrontendState::new();
9215 frontend.program = program;
9216
9217 let outcome = empty_exec_outcome_with_var_solutions(vec![(
9219 var_20.0,
9220 Some(var_10.1),
9221 Number {
9222 value: 33.0,
9223 units: NumericSuffix::Mm,
9224 },
9225 )]);
9226
9227 let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9228
9229 insta::assert_snapshot!(
9230 "test_commit_var_solution_node_path_wins_when_source_range_points_at_wrong_var",
9231 source_delta.text
9232 );
9233 }
9234
9235 #[test]
9242 fn test_commit_var_solution_writes_back_into_bare_var() {
9243 let initial_source = "\
9244@settings(experimentalFeatures = allow, kclVersion = 2.0)
9245sketch(on = XY) {
9246 line1 = line(start = [var, var 0mm], end = [var 10mm, var 0])
9247}
9248";
9249 let program = Program::parse(initial_source).unwrap().0.unwrap();
9250
9251 let bare = collect_all_sketch_vars(&program)
9254 .into_iter()
9255 .find(|(range, _)| {
9256 range.end() - range.start() == 3
9258 })
9259 .expect("expected at least one bare `var`");
9260
9261 let mut frontend = FrontendState::new();
9262 frontend.program = program;
9263
9264 let outcome = empty_exec_outcome_with_var_solutions(vec![(
9265 bare.0,
9266 Some(bare.1),
9267 Number {
9268 value: 7.0,
9269 units: NumericSuffix::Mm,
9270 },
9271 )]);
9272
9273 let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9274
9275 insta::assert_snapshot!("test_commit_var_solution_writes_back_into_bare_var", source_delta.text);
9279 }
9280
9281 #[tokio::test(flavor = "multi_thread")]
9282 async fn test_delete_point_without_var() {
9283 let initial_source = "\
9284sketch(on = XY) {
9285 point(at = [var 1, var 2])
9286 point(at = [var 3, var 4])
9287 point(at = [var 5, var 6])
9288}
9289";
9290
9291 let program = Program::parse(initial_source).unwrap().0.unwrap();
9292
9293 let mut frontend = FrontendState::new();
9294
9295 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9296 let mock_ctx = ExecutorContext::new_mock(None).await;
9297 let version = Version(0);
9298
9299 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9300 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9301 let sketch_id = sketch_object.id;
9302 let sketch = expect_sketch(sketch_object);
9303
9304 let point_id = *sketch.segments.get(1).unwrap();
9305
9306 let (src_delta, scene_delta) = frontend
9307 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
9308 .await
9309 .unwrap();
9310 insta::assert_snapshot!("test_delete_point_without_var", src_delta.text.as_str());
9311 assert_eq!(scene_delta.new_objects, vec![]);
9312 assert_eq!(scene_delta.new_graph.objects.len(), 4);
9313
9314 ctx.close().await;
9315 mock_ctx.close().await;
9316 }
9317
9318 #[tokio::test(flavor = "multi_thread")]
9319 async fn test_delete_point_with_var() {
9320 let initial_source = "\
9321sketch(on = XY) {
9322 point(at = [var 1, var 2])
9323 point1 = point(at = [var 3, var 4])
9324 point(at = [var 5, var 6])
9325}
9326";
9327
9328 let program = Program::parse(initial_source).unwrap().0.unwrap();
9329
9330 let mut frontend = FrontendState::new();
9331
9332 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9333 let mock_ctx = ExecutorContext::new_mock(None).await;
9334 let version = Version(0);
9335
9336 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9337 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9338 let sketch_id = sketch_object.id;
9339 let sketch = expect_sketch(sketch_object);
9340
9341 let point_id = *sketch.segments.get(1).unwrap();
9342
9343 let (src_delta, scene_delta) = frontend
9344 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
9345 .await
9346 .unwrap();
9347 insta::assert_snapshot!("test_delete_point_with_var", src_delta.text.as_str());
9348 assert_eq!(scene_delta.new_objects, vec![]);
9349 assert_eq!(scene_delta.new_graph.objects.len(), 4);
9350
9351 ctx.close().await;
9352 mock_ctx.close().await;
9353 }
9354
9355 #[tokio::test(flavor = "multi_thread")]
9356 async fn test_delete_multiple_points() {
9357 let initial_source = "\
9358sketch(on = XY) {
9359 point(at = [var 1, var 2])
9360 point1 = point(at = [var 3, var 4])
9361 point(at = [var 5, var 6])
9362}
9363";
9364
9365 let program = Program::parse(initial_source).unwrap().0.unwrap();
9366
9367 let mut frontend = FrontendState::new();
9368
9369 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9370 let mock_ctx = ExecutorContext::new_mock(None).await;
9371 let version = Version(0);
9372
9373 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9374 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9375 let sketch_id = sketch_object.id;
9376
9377 let sketch = expect_sketch(sketch_object);
9378
9379 let point1_id = *sketch.segments.first().unwrap();
9380 let point2_id = *sketch.segments.get(1).unwrap();
9381
9382 let (src_delta, scene_delta) = frontend
9383 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point1_id, point2_id])
9384 .await
9385 .unwrap();
9386 insta::assert_snapshot!("test_delete_multiple_points", src_delta.text.as_str());
9387 assert_eq!(scene_delta.new_objects, vec![]);
9388 assert_eq!(scene_delta.new_graph.objects.len(), 3);
9389
9390 ctx.close().await;
9391 mock_ctx.close().await;
9392 }
9393
9394 #[tokio::test(flavor = "multi_thread")]
9395 async fn test_delete_coincident_constraint() {
9396 let initial_source = "\
9397sketch(on = XY) {
9398 point1 = point(at = [var 1, var 2])
9399 point2 = point(at = [var 3, var 4])
9400 coincident([point1, point2])
9401 point(at = [var 5, var 6])
9402}
9403";
9404
9405 let program = Program::parse(initial_source).unwrap().0.unwrap();
9406
9407 let mut frontend = FrontendState::new();
9408
9409 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9410 let mock_ctx = ExecutorContext::new_mock(None).await;
9411 let version = Version(0);
9412
9413 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9414 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9415 let sketch_id = sketch_object.id;
9416 let sketch = expect_sketch(sketch_object);
9417
9418 let coincident_id = *sketch.constraints.first().unwrap();
9419
9420 let (src_delta, scene_delta) = frontend
9421 .delete_objects(&mock_ctx, version, sketch_id, vec![coincident_id], Vec::new())
9422 .await
9423 .unwrap();
9424 insta::assert_snapshot!("test_delete_coincident_constraint", src_delta.text.as_str());
9425 assert_eq!(scene_delta.new_objects, vec![]);
9426 assert_eq!(scene_delta.new_graph.objects.len(), 5);
9427
9428 ctx.close().await;
9429 mock_ctx.close().await;
9430 }
9431
9432 #[tokio::test(flavor = "multi_thread")]
9433 async fn test_delete_line_cascades_to_coincident_constraint() {
9434 let initial_source = "\
9435sketch(on = XY) {
9436 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9437 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9438 coincident([line1.end, line2.start])
9439}
9440";
9441
9442 let program = Program::parse(initial_source).unwrap().0.unwrap();
9443
9444 let mut frontend = FrontendState::new();
9445
9446 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9447 let mock_ctx = ExecutorContext::new_mock(None).await;
9448 let version = Version(0);
9449
9450 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9451 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9452 let sketch_id = sketch_object.id;
9453 let sketch = expect_sketch(sketch_object);
9454 let line_id = *sketch.segments.get(5).unwrap();
9455
9456 let (src_delta, scene_delta) = frontend
9457 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
9458 .await
9459 .unwrap();
9460 insta::assert_snapshot!(
9461 "test_delete_line_cascades_to_coincident_constraint",
9462 src_delta.text.as_str()
9463 );
9464 assert_eq!(
9465 scene_delta.new_graph.objects.len(),
9466 5,
9467 "{:#?}",
9468 scene_delta.new_graph.objects
9469 );
9470
9471 ctx.close().await;
9472 mock_ctx.close().await;
9473 }
9474
9475 #[tokio::test(flavor = "multi_thread")]
9476 async fn test_delete_line_cascades_to_distance_constraint() {
9477 let initial_source = "\
9478sketch(on = XY) {
9479 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9480 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9481 distance([line1.end, line2.start]) == 10mm
9482}
9483";
9484
9485 let program = Program::parse(initial_source).unwrap().0.unwrap();
9486
9487 let mut frontend = FrontendState::new();
9488
9489 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9490 let mock_ctx = ExecutorContext::new_mock(None).await;
9491 let version = Version(0);
9492
9493 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9494 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9495 let sketch_id = sketch_object.id;
9496 let sketch = expect_sketch(sketch_object);
9497 let line_id = *sketch.segments.get(5).unwrap();
9498
9499 let (src_delta, scene_delta) = frontend
9500 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
9501 .await
9502 .unwrap();
9503 insta::assert_snapshot!(
9504 "test_delete_line_cascades_to_distance_constraint",
9505 src_delta.text.as_str()
9506 );
9507 assert_eq!(
9508 scene_delta.new_graph.objects.len(),
9509 5,
9510 "{:#?}",
9511 scene_delta.new_graph.objects
9512 );
9513
9514 ctx.close().await;
9515 mock_ctx.close().await;
9516 }
9517
9518 #[tokio::test(flavor = "multi_thread")]
9519 async fn test_delete_point_cascades_to_horizontal_distance_constraint() {
9520 let initial_source = "\
9521sketch(on = XY) {
9522 point1 = point(at = [var 1, var 2])
9523 point2 = point(at = [var 3, var 4])
9524 horizontalDistance([point1, point2]) == 10mm
9525}
9526";
9527
9528 let program = Program::parse(initial_source).unwrap().0.unwrap();
9529
9530 let mut frontend = FrontendState::new();
9531
9532 let mock_ctx = ExecutorContext::new_mock(None).await;
9533 let version = Version(0);
9534
9535 frontend.program = program.clone();
9536 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9537 frontend.update_state_after_exec(outcome, true);
9538 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9539 let sketch_id = sketch_object.id;
9540 let sketch = expect_sketch(sketch_object);
9541 let point2_id = *sketch.segments.get(1).unwrap();
9542
9543 let (src_delta, scene_delta) = frontend
9544 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point2_id])
9545 .await
9546 .unwrap();
9547 insta::assert_snapshot!(
9548 "test_delete_point_cascades_to_horizontal_distance_constraint",
9549 src_delta.text.as_str()
9550 );
9551 assert_eq!(
9552 scene_delta.new_graph.objects.len(),
9553 3,
9554 "{:#?}",
9555 scene_delta.new_graph.objects
9556 );
9557
9558 mock_ctx.close().await;
9559 }
9560
9561 #[tokio::test(flavor = "multi_thread")]
9562 async fn test_delete_line_cascades_to_fixed_constraint() {
9563 let initial_source = "\
9564sketch(on = XY) {
9565 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9566 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9567 fixed([line1.start, [0, 0]])
9568}
9569";
9570
9571 let program = Program::parse(initial_source).unwrap().0.unwrap();
9572
9573 let mut frontend = FrontendState::new();
9574
9575 let mock_ctx = ExecutorContext::new_mock(None).await;
9576 let version = Version(0);
9577
9578 frontend.program = program.clone();
9579 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9580 frontend.update_state_after_exec(outcome, true);
9581 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9582 let sketch_id = sketch_object.id;
9583 let sketch = expect_sketch(sketch_object);
9584 let line1_id = *sketch.segments.get(2).unwrap();
9585
9586 let (src_delta, scene_delta) = frontend
9587 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
9588 .await
9589 .unwrap();
9590 insta::assert_snapshot!("test_delete_line_cascades_to_fixed_constraint", src_delta.text.as_str());
9591 assert_eq!(
9592 scene_delta.new_graph.objects.len(),
9593 5,
9594 "{:#?}",
9595 scene_delta.new_graph.objects
9596 );
9597
9598 mock_ctx.close().await;
9599 }
9600
9601 #[tokio::test(flavor = "multi_thread")]
9602 async fn test_delete_line_cascades_to_midpoint_constraint() {
9603 let initial_source = "\
9604sketch(on = XY) {
9605 point1 = point(at = [var 1, var 2])
9606 line1 = line(start = [var 0, var 0], end = [var 6, var 4])
9607 midpoint(line1, point = point1)
9608}
9609";
9610
9611 let program = Program::parse(initial_source).unwrap().0.unwrap();
9612
9613 let mut frontend = FrontendState::new();
9614
9615 let mock_ctx = ExecutorContext::new_mock(None).await;
9616 let version = Version(0);
9617
9618 frontend.program = program.clone();
9619 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9620 frontend.update_state_after_exec(outcome, true);
9621 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9622 let sketch_id = sketch_object.id;
9623 let sketch = expect_sketch(sketch_object);
9624 let line1_id = *sketch.segments.get(3).unwrap();
9625
9626 let (src_delta, scene_delta) = frontend
9627 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
9628 .await
9629 .unwrap();
9630 insta::assert_snapshot!(
9631 "test_delete_line_cascades_to_midpoint_constraint",
9632 src_delta.text.as_str()
9633 );
9634 assert_eq!(
9635 scene_delta.new_graph.objects.len(),
9636 3,
9637 "{:#?}",
9638 scene_delta.new_graph.objects
9639 );
9640
9641 mock_ctx.close().await;
9642 }
9643
9644 #[tokio::test(flavor = "multi_thread")]
9645 async fn test_delete_point_preserves_multiline_coincident_constraint() {
9646 let initial_source = "\
9647sketch(on = XY) {
9648 point1 = point(at = [var 1, var 2])
9649 point2 = point(at = [var 3, var 4])
9650 point3 = point(at = [var 5, var 6])
9651 coincident([point1, point2, point3])
9652}
9653";
9654
9655 let program = Program::parse(initial_source).unwrap().0.unwrap();
9656
9657 let mut frontend = FrontendState::new();
9658
9659 let mock_ctx = ExecutorContext::new_mock(None).await;
9660 let version = Version(0);
9661
9662 frontend.program = program.clone();
9663 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9664 frontend.update_state_after_exec(outcome, true);
9665 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9666 let sketch_id = sketch_object.id;
9667 let sketch = expect_sketch(sketch_object);
9668 let point3_id = *sketch.segments.get(2).unwrap();
9669
9670 let (src_delta, scene_delta) = frontend
9671 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point3_id])
9672 .await
9673 .unwrap();
9674 assert!(src_delta.text.contains("point1 = point("), "{}", src_delta.text);
9675 assert!(src_delta.text.contains("point2 = point("), "{}", src_delta.text);
9676 assert!(!src_delta.text.contains("point3 = point("), "{}", src_delta.text);
9677 assert!(
9678 src_delta.text.contains("coincident([point1, point2])"),
9679 "{}",
9680 src_delta.text
9681 );
9682
9683 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9684 let sketch = expect_sketch(sketch_object);
9685 assert_eq!(sketch.segments.len(), 2);
9686 assert_eq!(sketch.constraints.len(), 1);
9687
9688 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
9689 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9690 panic!("Expected constraint object");
9691 };
9692 let Constraint::Coincident(coincident) = constraint else {
9693 panic!("Expected coincident constraint");
9694 };
9695 assert_eq!(
9696 coincident.segments,
9697 sketch
9698 .segments
9699 .iter()
9700 .copied()
9701 .map(Into::into)
9702 .collect::<Vec<ConstraintSegment>>()
9703 );
9704
9705 mock_ctx.close().await;
9706 }
9707
9708 #[tokio::test(flavor = "multi_thread")]
9709 async fn test_delete_line_preserves_multiline_equal_length_constraint() {
9710 let initial_source = "\
9711sketch(on = XY) {
9712 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9713 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9714 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
9715 equalLength([line1, line2, line3])
9716}
9717";
9718
9719 let program = Program::parse(initial_source).unwrap().0.unwrap();
9720
9721 let mut frontend = FrontendState::new();
9722
9723 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9724 let mock_ctx = ExecutorContext::new_mock(None).await;
9725 let version = Version(0);
9726
9727 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9728 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9729 let sketch_id = sketch_object.id;
9730 let sketch = expect_sketch(sketch_object);
9731 let line3_id = *sketch.segments.get(8).unwrap();
9732
9733 let (src_delta, scene_delta) = frontend
9734 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line3_id])
9735 .await
9736 .unwrap();
9737 insta::assert_snapshot!(
9738 "test_delete_line_preserves_multiline_equal_length_constraint",
9739 src_delta.text.as_str()
9740 );
9741
9742 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9743 let sketch = expect_sketch(sketch_object);
9744 assert_eq!(sketch.constraints.len(), 1);
9745
9746 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
9747 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9748 panic!("Expected constraint object");
9749 };
9750 let Constraint::LinesEqualLength(lines_equal_length) = constraint else {
9751 panic!("Expected lines equal length constraint");
9752 };
9753 assert_eq!(lines_equal_length.lines.len(), 2);
9754
9755 ctx.close().await;
9756 mock_ctx.close().await;
9757 }
9758
9759 #[tokio::test(flavor = "multi_thread")]
9760 async fn test_delete_line_preserves_multiline_horizontal_constraint() {
9761 let initial_source = "\
9762sketch(on = XY) {
9763 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9764 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9765 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
9766 horizontal([line1.end, line2.start, line3.start])
9767}
9768";
9769
9770 let program = Program::parse(initial_source).unwrap().0.unwrap();
9771
9772 let mut frontend = FrontendState::new();
9773
9774 let mock_ctx = ExecutorContext::new_mock(None).await;
9775 let version = Version(0);
9776
9777 frontend.program = program.clone();
9778 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9779 frontend.update_state_after_exec(outcome, true);
9780 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9781 let sketch_id = sketch_object.id;
9782 let sketch = expect_sketch(sketch_object);
9783 let line1_id = *sketch.segments.get(2).unwrap();
9784
9785 let (src_delta, scene_delta) = frontend
9786 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
9787 .await
9788 .unwrap();
9789 assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
9790 assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
9791 assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
9792 assert!(
9793 src_delta.text.contains("horizontal([line2.start, line3.start])"),
9794 "{}",
9795 src_delta.text
9796 );
9797
9798 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9799 let sketch = expect_sketch(sketch_object);
9800 assert_eq!(sketch.constraints.len(), 1);
9801
9802 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
9803 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9804 panic!("Expected constraint object");
9805 };
9806 let Constraint::Horizontal(Horizontal::Points { points }) = constraint else {
9807 panic!("Expected horizontal points constraint");
9808 };
9809 let remaining_points = vec![sketch.segments[0].into(), sketch.segments[3].into()];
9810 assert_eq!(*points, remaining_points);
9811
9812 mock_ctx.close().await;
9813 }
9814
9815 #[tokio::test(flavor = "multi_thread")]
9816 async fn test_delete_line_preserves_multiline_vertical_constraint() {
9817 let initial_source = "\
9818sketch(on = XY) {
9819 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9820 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9821 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
9822 vertical([line1.end, line2.start, line3.start])
9823}
9824";
9825
9826 let program = Program::parse(initial_source).unwrap().0.unwrap();
9827
9828 let mut frontend = FrontendState::new();
9829
9830 let mock_ctx = ExecutorContext::new_mock(None).await;
9831 let version = Version(0);
9832
9833 frontend.program = program.clone();
9834 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9835 frontend.update_state_after_exec(outcome, true);
9836 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9837 let sketch_id = sketch_object.id;
9838 let sketch = expect_sketch(sketch_object);
9839 let line1_id = *sketch.segments.get(2).unwrap();
9840
9841 let (src_delta, scene_delta) = frontend
9842 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
9843 .await
9844 .unwrap();
9845 assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
9846 assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
9847 assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
9848 assert!(
9849 src_delta.text.contains("vertical([line2.start, line3.start])"),
9850 "{}",
9851 src_delta.text
9852 );
9853
9854 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9855 let sketch = expect_sketch(sketch_object);
9856 assert_eq!(sketch.constraints.len(), 1);
9857
9858 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
9859 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9860 panic!("Expected constraint object");
9861 };
9862 let Constraint::Vertical(Vertical::Points { points }) = constraint else {
9863 panic!("Expected vertical points constraint");
9864 };
9865 let remaining_points = vec![sketch.segments[0].into(), sketch.segments[3].into()];
9866 assert_eq!(*points, remaining_points);
9867
9868 mock_ctx.close().await;
9869 }
9870
9871 #[tokio::test(flavor = "multi_thread")]
9872 async fn test_delete_line_preserves_multiline_coincident_constraint() {
9873 let initial_source = "\
9874sketch(on = XY) {
9875 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9876 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9877 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
9878 coincident([line1.end, line2.start, line3.start])
9879}
9880";
9881
9882 let program = Program::parse(initial_source).unwrap().0.unwrap();
9883
9884 let mut frontend = FrontendState::new();
9885
9886 let mock_ctx = ExecutorContext::new_mock(None).await;
9887 let version = Version(0);
9888
9889 frontend.program = program.clone();
9890 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9891 frontend.update_state_after_exec(outcome, true);
9892 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9893 let sketch_id = sketch_object.id;
9894 let sketch = expect_sketch(sketch_object);
9895 let line1_id = *sketch.segments.get(2).unwrap();
9896
9897 let (src_delta, scene_delta) = frontend
9898 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
9899 .await
9900 .unwrap();
9901 assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
9902 assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
9903 assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
9904 assert!(
9905 src_delta.text.contains("coincident([line2.start, line3.start])"),
9906 "{}",
9907 src_delta.text
9908 );
9909
9910 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9911 let sketch = expect_sketch(sketch_object);
9912 assert_eq!(sketch.constraints.len(), 1);
9913
9914 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
9915 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9916 panic!("Expected constraint object");
9917 };
9918 let Constraint::Coincident(coincident) = constraint else {
9919 panic!("Expected coincident constraint");
9920 };
9921 let remaining_segments = vec![sketch.segments[0].into(), sketch.segments[3].into()];
9922 assert_eq!(coincident.segments, remaining_segments);
9923
9924 mock_ctx.close().await;
9925 }
9926
9927 #[tokio::test(flavor = "multi_thread")]
9928 async fn test_delete_lines_removes_multiline_equal_length_constraint_below_minimum() {
9929 let initial_source = "\
9930sketch(on = XY) {
9931 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9932 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9933 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
9934 equalLength([line1, line2, line3])
9935}
9936";
9937
9938 let program = Program::parse(initial_source).unwrap().0.unwrap();
9939
9940 let mut frontend = FrontendState::new();
9941
9942 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9943 let mock_ctx = ExecutorContext::new_mock(None).await;
9944 let version = Version(0);
9945
9946 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9947 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9948 let sketch_id = sketch_object.id;
9949 let sketch = expect_sketch(sketch_object);
9950 let line2_id = *sketch.segments.get(5).unwrap();
9951 let line3_id = *sketch.segments.get(8).unwrap();
9952
9953 let (src_delta, scene_delta) = frontend
9954 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line2_id, line3_id])
9955 .await
9956 .unwrap();
9957 insta::assert_snapshot!(
9958 "test_delete_lines_removes_multiline_equal_length_constraint_below_minimum",
9959 src_delta.text.as_str()
9960 );
9961
9962 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9963 let sketch = expect_sketch(sketch_object);
9964 assert!(sketch.constraints.is_empty());
9965
9966 ctx.close().await;
9967 mock_ctx.close().await;
9968 }
9969
9970 #[tokio::test(flavor = "multi_thread")]
9971 async fn test_delete_line_preserves_multiline_parallel_constraint() {
9972 let initial_source = "\
9973sketch(on = XY) {
9974 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9975 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9976 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
9977 parallel([line1, line2, line3])
9978}
9979";
9980
9981 let program = Program::parse(initial_source).unwrap().0.unwrap();
9982
9983 let mut frontend = FrontendState::new();
9984
9985 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9986 let mock_ctx = ExecutorContext::new_mock(None).await;
9987 let version = Version(0);
9988
9989 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9990 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9991 let sketch_id = sketch_object.id;
9992 let sketch = expect_sketch(sketch_object);
9993 let line3_id = *sketch.segments.get(8).unwrap();
9994
9995 let (src_delta, scene_delta) = frontend
9996 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line3_id])
9997 .await
9998 .unwrap();
9999 insta::assert_snapshot!(
10000 "test_delete_line_preserves_multiline_parallel_constraint",
10001 src_delta.text.as_str()
10002 );
10003
10004 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10005 let sketch = expect_sketch(sketch_object);
10006 assert_eq!(sketch.constraints.len(), 1);
10007
10008 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10009 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10010 panic!("Expected constraint object");
10011 };
10012 let Constraint::Parallel(parallel) = constraint else {
10013 panic!("Expected parallel constraint");
10014 };
10015 assert_eq!(parallel.lines.len(), 2);
10016
10017 ctx.close().await;
10018 mock_ctx.close().await;
10019 }
10020
10021 #[tokio::test(flavor = "multi_thread")]
10022 async fn test_delete_lines_removes_multiline_parallel_constraint_below_minimum() {
10023 let initial_source = "\
10024sketch(on = XY) {
10025 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10026 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10027 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10028 parallel([line1, line2, line3])
10029}
10030";
10031
10032 let program = Program::parse(initial_source).unwrap().0.unwrap();
10033
10034 let mut frontend = FrontendState::new();
10035
10036 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10037 let mock_ctx = ExecutorContext::new_mock(None).await;
10038 let version = Version(0);
10039
10040 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10041 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10042 let sketch_id = sketch_object.id;
10043 let sketch = expect_sketch(sketch_object);
10044 let line2_id = *sketch.segments.get(5).unwrap();
10045 let line3_id = *sketch.segments.get(8).unwrap();
10046
10047 let (src_delta, scene_delta) = frontend
10048 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line2_id, line3_id])
10049 .await
10050 .unwrap();
10051 insta::assert_snapshot!(
10052 "test_delete_lines_removes_multiline_parallel_constraint_below_minimum",
10053 src_delta.text.as_str()
10054 );
10055
10056 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10057 let sketch = expect_sketch(sketch_object);
10058 assert!(sketch.constraints.is_empty());
10059
10060 ctx.close().await;
10061 mock_ctx.close().await;
10062 }
10063
10064 #[tokio::test(flavor = "multi_thread")]
10065 async fn test_delete_line_line_coincident_constraint() {
10066 let initial_source = "\
10067sketch(on = XY) {
10068 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10069 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10070 coincident([line1, line2])
10071}
10072";
10073
10074 let program = Program::parse(initial_source).unwrap().0.unwrap();
10075
10076 let mut frontend = FrontendState::new();
10077
10078 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10079 let mock_ctx = ExecutorContext::new_mock(None).await;
10080 let version = Version(0);
10081
10082 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10083 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10084 let sketch_id = sketch_object.id;
10085 let sketch = expect_sketch(sketch_object);
10086
10087 let coincident_id = *sketch.constraints.first().unwrap();
10088
10089 let (src_delta, scene_delta) = frontend
10090 .delete_objects(&mock_ctx, version, sketch_id, vec![coincident_id], Vec::new())
10091 .await
10092 .unwrap();
10093 insta::assert_snapshot!("test_delete_line_line_coincident_constraint", src_delta.text.as_str());
10094 assert_eq!(scene_delta.new_objects, vec![]);
10095 assert_eq!(scene_delta.new_graph.objects.len(), 8);
10096
10097 ctx.close().await;
10098 mock_ctx.close().await;
10099 }
10100
10101 #[tokio::test(flavor = "multi_thread")]
10102 async fn test_two_points_coincident() {
10103 let initial_source = "\
10104sketch(on = XY) {
10105 point1 = point(at = [var 1, var 2])
10106 point(at = [3, 4])
10107}
10108";
10109
10110 let program = Program::parse(initial_source).unwrap().0.unwrap();
10111
10112 let mut frontend = FrontendState::new();
10113
10114 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10115 let mock_ctx = ExecutorContext::new_mock(None).await;
10116 let version = Version(0);
10117
10118 frontend.hack_set_program(&ctx, program).await.unwrap();
10119 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10120 let sketch_id = sketch_object.id;
10121 let sketch = expect_sketch(sketch_object);
10122 let point0_id = *sketch.segments.first().unwrap();
10123 let point1_id = *sketch.segments.get(1).unwrap();
10124
10125 let constraint = Constraint::Coincident(Coincident {
10126 segments: vec![point0_id.into(), point1_id.into()],
10127 });
10128 let (src_delta, scene_delta) = frontend
10129 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10130 .await
10131 .unwrap();
10132 insta::assert_snapshot!("test_two_points_coincident", src_delta.text.as_str());
10133 assert_eq!(
10134 scene_delta.new_graph.objects.len(),
10135 5,
10136 "{:#?}",
10137 scene_delta.new_graph.objects
10138 );
10139
10140 ctx.close().await;
10141 mock_ctx.close().await;
10142 }
10143
10144 #[tokio::test(flavor = "multi_thread")]
10145 async fn test_three_points_coincident() {
10146 let initial_source = "\
10147sketch(on = XY) {
10148 point1 = point(at = [var 1, var 2])
10149 point(at = [var 3, var 4])
10150 point(at = [var 5, var 6])
10151}
10152";
10153
10154 let program = Program::parse(initial_source).unwrap().0.unwrap();
10155
10156 let mut frontend = FrontendState::new();
10157
10158 let mock_ctx = ExecutorContext::new_mock(None).await;
10159 let version = Version(0);
10160
10161 frontend.program = program.clone();
10162 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10163 frontend.update_state_after_exec(outcome, true);
10164 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10165 let sketch_id = sketch_object.id;
10166 let sketch = expect_sketch(sketch_object);
10167 let segments = sketch
10168 .segments
10169 .iter()
10170 .take(3)
10171 .copied()
10172 .map(Into::into)
10173 .collect::<Vec<ConstraintSegment>>();
10174
10175 let constraint = Constraint::Coincident(Coincident {
10176 segments: segments.clone(),
10177 });
10178 let (src_delta, scene_delta) = frontend
10179 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10180 .await
10181 .unwrap();
10182 insta::assert_snapshot!("test_three_points_coincident", src_delta.text.as_str());
10183
10184 let constraint_object = scene_delta
10185 .new_graph
10186 .objects
10187 .iter()
10188 .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
10189 .unwrap();
10190
10191 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10192 panic!("expected a constraint object");
10193 };
10194
10195 assert_eq!(constraint, &Constraint::Coincident(Coincident { segments }));
10196
10197 mock_ctx.close().await;
10198 }
10199
10200 #[tokio::test(flavor = "multi_thread")]
10201 async fn test_source_with_three_point_coincident_tracks_all_segments() {
10202 let initial_source = "\
10203sketch(on = XY) {
10204 point1 = point(at = [var 1, var 2])
10205 point2 = point(at = [var 3, var 4])
10206 point3 = point(at = [var 5, var 6])
10207 coincident([point1, point2, point3])
10208}
10209";
10210
10211 let program = Program::parse(initial_source).unwrap().0.unwrap();
10212
10213 let mut frontend = FrontendState::new();
10214
10215 let ctx = ExecutorContext::new_mock(None).await;
10216 frontend.program = program.clone();
10217 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10218 frontend.update_state_after_exec(outcome, true);
10219
10220 let constraint_object = frontend
10221 .scene_graph
10222 .objects
10223 .iter()
10224 .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
10225 .unwrap();
10226 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10227 panic!("expected a constraint object");
10228 };
10229
10230 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10231 let sketch = expect_sketch(sketch_object);
10232 let expected_segments = sketch
10233 .segments
10234 .iter()
10235 .take(3)
10236 .copied()
10237 .map(Into::into)
10238 .collect::<Vec<ConstraintSegment>>();
10239
10240 assert_eq!(
10241 constraint,
10242 &Constraint::Coincident(Coincident {
10243 segments: expected_segments,
10244 })
10245 );
10246
10247 ctx.close().await;
10248 }
10249
10250 #[tokio::test(flavor = "multi_thread")]
10251 async fn test_point_origin_coincident_preserves_order() {
10252 let initial_source = "\
10253sketch(on = XY) {
10254 point(at = [var 1, var 2])
10255}
10256";
10257
10258 for (origin_first, snapshot_name) in [
10259 (true, "test_point_origin_coincident_preserves_order_origin_first"),
10260 (false, "test_point_origin_coincident_preserves_order_point_first"),
10261 ] {
10262 let program = Program::parse(initial_source).unwrap().0.unwrap();
10263
10264 let mut frontend = FrontendState::new();
10265
10266 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10267 let mock_ctx = ExecutorContext::new_mock(None).await;
10268 let version = Version(0);
10269
10270 frontend.hack_set_program(&ctx, program).await.unwrap();
10271 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10272 let sketch_id = sketch_object.id;
10273 let sketch = expect_sketch(sketch_object);
10274 let point_id = *sketch.segments.first().unwrap();
10275
10276 let segments = if origin_first {
10277 vec![ConstraintSegment::ORIGIN, point_id.into()]
10278 } else {
10279 vec![point_id.into(), ConstraintSegment::ORIGIN]
10280 };
10281 let constraint = Constraint::Coincident(Coincident {
10282 segments: segments.clone(),
10283 });
10284 let (src_delta, scene_delta) = frontend
10285 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10286 .await
10287 .unwrap();
10288 insta::assert_snapshot!(snapshot_name, src_delta.text.as_str());
10289
10290 let constraint_object = scene_delta
10291 .new_graph
10292 .objects
10293 .iter()
10294 .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
10295 .unwrap();
10296
10297 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10298 panic!("expected a constraint object");
10299 };
10300
10301 assert_eq!(constraint, &Constraint::Coincident(Coincident { segments }));
10302
10303 ctx.close().await;
10304 mock_ctx.close().await;
10305 }
10306 }
10307
10308 #[tokio::test(flavor = "multi_thread")]
10309 async fn test_coincident_of_line_end_points() {
10310 let initial_source = "\
10311sketch(on = XY) {
10312 line(start = [var 1, var 2], end = [var 3, var 4])
10313 line(start = [var 5, var 6], end = [var 7, var 8])
10314}
10315";
10316
10317 let program = Program::parse(initial_source).unwrap().0.unwrap();
10318
10319 let mut frontend = FrontendState::new();
10320
10321 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10322 let mock_ctx = ExecutorContext::new_mock(None).await;
10323 let version = Version(0);
10324
10325 frontend.hack_set_program(&ctx, program).await.unwrap();
10326 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10327 let sketch_id = sketch_object.id;
10328 let sketch = expect_sketch(sketch_object);
10329 let point0_id = *sketch.segments.get(1).unwrap();
10330 let point1_id = *sketch.segments.get(3).unwrap();
10331
10332 let constraint = Constraint::Coincident(Coincident {
10333 segments: vec![point0_id.into(), point1_id.into()],
10334 });
10335 let (src_delta, scene_delta) = frontend
10336 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10337 .await
10338 .unwrap();
10339 insta::assert_snapshot!("test_coincident_of_line_end_points", src_delta.text.as_str());
10340 assert_eq!(
10341 scene_delta.new_graph.objects.len(),
10342 9,
10343 "{:#?}",
10344 scene_delta.new_graph.objects
10345 );
10346
10347 ctx.close().await;
10348 mock_ctx.close().await;
10349 }
10350
10351 #[tokio::test(flavor = "multi_thread")]
10352 async fn test_coincident_of_line_point_and_circle_segment() {
10353 let initial_source = "\
10354sketch(on = XY) {
10355 circle1 = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
10356 line1 = line(start = [var 9mm, var 1mm], end = [var 10mm, var 2mm])
10357}
10358";
10359 let program = Program::parse(initial_source).unwrap().0.unwrap();
10360 let mut frontend = FrontendState::new();
10361
10362 let mock_ctx = ExecutorContext::new_mock(None).await;
10363 let version = Version(0);
10364
10365 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10366 frontend.program = program;
10367 frontend.update_state_after_exec(outcome, true);
10368 let sketch_object = find_first_sketch_object(&frontend.scene_graph).expect("Expected sketch object");
10369 let sketch_id = sketch_object.id;
10370 let sketch = expect_sketch(sketch_object);
10371
10372 let circle_id = sketch
10373 .segments
10374 .iter()
10375 .copied()
10376 .find(|seg_id| {
10377 matches!(
10378 &frontend.scene_graph.objects[seg_id.0].kind,
10379 ObjectKind::Segment {
10380 segment: Segment::Circle(_)
10381 }
10382 )
10383 })
10384 .expect("Expected a circle segment in sketch");
10385 let line_id = frontend
10386 .scene_graph
10387 .objects
10388 .iter()
10389 .find_map(|obj| match &obj.kind {
10390 ObjectKind::Segment {
10391 segment: Segment::Line(line),
10392 } if line.owner.is_none() => Some(obj.id),
10393 _ => None,
10394 })
10395 .expect("Expected a standalone line segment in scene graph");
10396
10397 let line_start_point_id = match &frontend.scene_graph.objects[line_id.0].kind {
10398 ObjectKind::Segment {
10399 segment: Segment::Line(line),
10400 } => line.start,
10401 _ => panic!("Expected line segment object"),
10402 };
10403
10404 let constraint = Constraint::Coincident(Coincident {
10405 segments: vec![line_start_point_id.into(), circle_id.into()],
10406 });
10407 let (src_delta, _scene_delta) = frontend
10408 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10409 .await
10410 .unwrap();
10411 insta::assert_snapshot!(
10412 "test_coincident_of_line_point_and_circle_segment",
10413 src_delta.text.as_str()
10414 );
10415
10416 mock_ctx.close().await;
10417 }
10418
10419 #[tokio::test(flavor = "multi_thread")]
10420 async fn test_invalid_coincident_arc_and_line_preserves_state() {
10421 let program = Program::empty();
10429
10430 let mut frontend = FrontendState::new();
10431 frontend.program = program;
10432
10433 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10434 let mock_ctx = ExecutorContext::new_mock(None).await;
10435 let version = Version(0);
10436
10437 let sketch_args = SketchCtor {
10438 on: Plane::Default(PlaneName::Xy),
10439 };
10440 let (_src_delta, _scene_delta, sketch_id) = frontend
10441 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
10442 .await
10443 .unwrap();
10444
10445 let arc_ctor = ArcCtor {
10447 start: Point2d {
10448 x: Expr::Var(Number {
10449 value: 0.0,
10450 units: NumericSuffix::Mm,
10451 }),
10452 y: Expr::Var(Number {
10453 value: 0.0,
10454 units: NumericSuffix::Mm,
10455 }),
10456 },
10457 end: Point2d {
10458 x: Expr::Var(Number {
10459 value: 10.0,
10460 units: NumericSuffix::Mm,
10461 }),
10462 y: Expr::Var(Number {
10463 value: 10.0,
10464 units: NumericSuffix::Mm,
10465 }),
10466 },
10467 center: Point2d {
10468 x: Expr::Var(Number {
10469 value: 10.0,
10470 units: NumericSuffix::Mm,
10471 }),
10472 y: Expr::Var(Number {
10473 value: 0.0,
10474 units: NumericSuffix::Mm,
10475 }),
10476 },
10477 construction: None,
10478 };
10479 let (_src_delta, scene_delta) = frontend
10480 .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Arc(arc_ctor), None)
10481 .await
10482 .unwrap();
10483 let arc_id = *scene_delta.new_objects.last().unwrap();
10485
10486 let line_ctor = LineCtor {
10488 start: Point2d {
10489 x: Expr::Var(Number {
10490 value: 20.0,
10491 units: NumericSuffix::Mm,
10492 }),
10493 y: Expr::Var(Number {
10494 value: 0.0,
10495 units: NumericSuffix::Mm,
10496 }),
10497 },
10498 end: Point2d {
10499 x: Expr::Var(Number {
10500 value: 30.0,
10501 units: NumericSuffix::Mm,
10502 }),
10503 y: Expr::Var(Number {
10504 value: 10.0,
10505 units: NumericSuffix::Mm,
10506 }),
10507 },
10508 construction: None,
10509 };
10510 let (_src_delta, scene_delta) = frontend
10511 .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Line(line_ctor), None)
10512 .await
10513 .unwrap();
10514 let line_id = *scene_delta.new_objects.last().unwrap();
10516
10517 let constraint = Constraint::Coincident(Coincident {
10520 segments: vec![arc_id.into(), line_id.into()],
10521 });
10522 let result = frontend.add_constraint(&mock_ctx, version, sketch_id, constraint).await;
10523
10524 assert!(result.is_err(), "Expected invalid coincident constraint to fail");
10526
10527 let sketch_object_after =
10530 find_first_sketch_object(&frontend.scene_graph).expect("Sketch should still exist after failed constraint");
10531 let sketch_after = expect_sketch(sketch_object_after);
10532
10533 assert!(
10535 sketch_after.segments.contains(&arc_id),
10536 "Arc segment should still exist after failed constraint"
10537 );
10538 assert!(
10539 sketch_after.segments.contains(&line_id),
10540 "Line segment should still exist after failed constraint"
10541 );
10542
10543 let arc_obj = frontend
10545 .scene_graph
10546 .objects
10547 .get(arc_id.0)
10548 .expect("Arc object should still be accessible");
10549 let line_obj = frontend
10550 .scene_graph
10551 .objects
10552 .get(line_id.0)
10553 .expect("Line object should still be accessible");
10554
10555 match &arc_obj.kind {
10558 ObjectKind::Segment {
10559 segment: Segment::Arc(_),
10560 } => {}
10561 _ => panic!("Arc object should still be an arc segment"),
10562 }
10563 match &line_obj.kind {
10564 ObjectKind::Segment {
10565 segment: Segment::Line(_),
10566 } => {}
10567 _ => panic!("Line object should still be a line segment"),
10568 }
10569
10570 ctx.close().await;
10571 mock_ctx.close().await;
10572 }
10573
10574 #[tokio::test(flavor = "multi_thread")]
10575 async fn test_distance_two_points() {
10576 let initial_source = "\
10577sketch(on = XY) {
10578 point(at = [var 1, var 2])
10579 point(at = [var 3, var 4])
10580}
10581";
10582
10583 let program = Program::parse(initial_source).unwrap().0.unwrap();
10584
10585 let mut frontend = FrontendState::new();
10586
10587 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10588 let mock_ctx = ExecutorContext::new_mock(None).await;
10589 let version = Version(0);
10590
10591 frontend.hack_set_program(&ctx, program).await.unwrap();
10592 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10593 let sketch_id = sketch_object.id;
10594 let sketch = expect_sketch(sketch_object);
10595 let point0_id = *sketch.segments.first().unwrap();
10596 let point1_id = *sketch.segments.get(1).unwrap();
10597
10598 let constraint = Constraint::Distance(Distance {
10599 points: vec![point0_id.into(), point1_id.into()],
10600 distance: Number {
10601 value: 2.0,
10602 units: NumericSuffix::Mm,
10603 },
10604 label_position: None,
10605 source: Default::default(),
10606 });
10607 let (src_delta, scene_delta) = frontend
10608 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10609 .await
10610 .unwrap();
10611 insta::assert_snapshot!("test_distance_two_points", src_delta.text.as_str());
10612 assert_eq!(
10613 scene_delta.new_graph.objects.len(),
10614 5,
10615 "{:#?}",
10616 scene_delta.new_graph.objects
10617 );
10618
10619 ctx.close().await;
10620 mock_ctx.close().await;
10621 }
10622
10623 #[tokio::test(flavor = "multi_thread")]
10624 async fn test_distance_two_points_with_label() {
10625 let initial_source = "\
10626sketch(on = XY) {
10627 point(at = [var 1, var 2])
10628 point(at = [var 3, var 4])
10629}
10630";
10631
10632 let program = Program::parse(initial_source).unwrap().0.unwrap();
10633
10634 let mut frontend = FrontendState::new();
10635
10636 let mock_ctx = ExecutorContext::new_mock(None).await;
10637 let version = Version(0);
10638
10639 frontend.program = program.clone();
10640 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10641 frontend.update_state_after_exec(outcome, true);
10642 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10643 let sketch_id = sketch_object.id;
10644 let sketch = expect_sketch(sketch_object);
10645 let point0_id = *sketch.segments.first().unwrap();
10646 let point1_id = *sketch.segments.get(1).unwrap();
10647
10648 let label_position = Point2d {
10649 x: Number {
10650 value: 10.0,
10651 units: NumericSuffix::Mm,
10652 },
10653 y: Number {
10654 value: 11.0,
10655 units: NumericSuffix::Mm,
10656 },
10657 };
10658 let constraint = Constraint::Distance(Distance {
10659 points: vec![point0_id.into(), point1_id.into()],
10660 distance: Number {
10661 value: 2.0,
10662 units: NumericSuffix::Mm,
10663 },
10664 label_position: Some(label_position.clone()),
10665 source: Default::default(),
10666 });
10667 let (src_delta, scene_delta) = frontend
10668 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10669 .await
10670 .unwrap();
10671 insta::assert_snapshot!("test_distance_two_points_with_label", src_delta.text.as_str());
10672
10673 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10674 let sketch = expect_sketch(sketch_object);
10675 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10676 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10677 panic!("Expected constraint object");
10678 };
10679 let Constraint::Distance(distance) = constraint else {
10680 panic!("Expected distance constraint");
10681 };
10682 assert_eq!(distance.label_position, Some(label_position));
10683
10684 mock_ctx.close().await;
10685 }
10686
10687 #[tokio::test(flavor = "multi_thread")]
10688 async fn test_edit_distance_constraint_label_position() {
10689 let initial_source = "\
10690sketch(on = XY) {
10691 point(at = [var 1, var 2])
10692 point(at = [var 3, var 2])
10693}
10694";
10695
10696 let program = Program::parse(initial_source).unwrap().0.unwrap();
10697
10698 let mut frontend = FrontendState::new();
10699
10700 let mock_ctx = ExecutorContext::new_mock(None).await;
10701 let version = Version(0);
10702
10703 frontend.program = program.clone();
10704 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10705 frontend.update_state_after_exec(outcome, true);
10706 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10707 let sketch_id = sketch_object.id;
10708 let sketch = expect_sketch(sketch_object);
10709 let point0_id = *sketch.segments.first().unwrap();
10710 let point1_id = *sketch.segments.get(1).unwrap();
10711
10712 let constraint = Constraint::Distance(Distance {
10713 points: vec![point0_id.into(), point1_id.into()],
10714 distance: Number {
10715 value: 2.0,
10716 units: NumericSuffix::Mm,
10717 },
10718 label_position: None,
10719 source: Default::default(),
10720 });
10721 let (_, scene_delta) = frontend
10722 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10723 .await
10724 .unwrap();
10725 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10726 let sketch = expect_sketch(sketch_object);
10727 let constraint_id = sketch.constraints[0];
10728 let label_position = Point2d {
10729 x: Number {
10730 value: 10.0,
10731 units: NumericSuffix::Mm,
10732 },
10733 y: Number {
10734 value: 11.0,
10735 units: NumericSuffix::Mm,
10736 },
10737 };
10738
10739 let (src_delta, scene_delta) = frontend
10740 .edit_distance_constraint_label_position(
10741 &mock_ctx,
10742 version,
10743 sketch_id,
10744 constraint_id,
10745 label_position.clone(),
10746 vec![],
10747 )
10748 .await
10749 .unwrap();
10750 insta::assert_snapshot!("test_edit_distance_constraint_label_position", src_delta.text.as_str());
10751
10752 let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
10753 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10754 panic!("Expected constraint object");
10755 };
10756 let Constraint::Distance(distance) = constraint else {
10757 panic!("Expected distance constraint");
10758 };
10759 assert_eq!(distance.label_position, Some(label_position));
10760
10761 mock_ctx.close().await;
10762 }
10763
10764 #[tokio::test(flavor = "multi_thread")]
10765 async fn test_edit_distance_constraint_label_position_preserves_anchor_segment_solution() {
10766 let initial_source = "\
10767sketch(on = XY) {
10768 point1 = point(at = [var 0mm, var 0mm])
10769 point2 = point(at = [var 10mm, var 0mm])
10770 distance([point1, point2]) == 5mm
10771}
10772";
10773
10774 let program = Program::parse(initial_source).unwrap().0.unwrap();
10775 let mut frontend = FrontendState::new();
10776 let mock_ctx = ExecutorContext::new_mock(None).await;
10777 let version = Version(0);
10778
10779 frontend.program = program.clone();
10780 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10781 frontend.update_state_after_exec(outcome, true);
10782 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10783 let sketch_id = sketch_object.id;
10784 let sketch = expect_sketch(sketch_object);
10785 let point0_id = sketch.segments[0];
10786 let point1_id = sketch.segments[1];
10787 let constraint_id = sketch.constraints[0];
10788
10789 let edited_segments = vec![ExistingSegmentCtor {
10790 id: point0_id,
10791 ctor: SegmentCtor::Point(PointCtor {
10792 position: Point2d {
10793 x: Expr::Var(Number {
10794 value: 2.0,
10795 units: NumericSuffix::Mm,
10796 }),
10797 y: Expr::Var(Number {
10798 value: 1.0,
10799 units: NumericSuffix::Mm,
10800 }),
10801 },
10802 }),
10803 }];
10804 let (_, scene_delta) = frontend
10805 .edit_segments(&mock_ctx, version, sketch_id, edited_segments)
10806 .await
10807 .unwrap();
10808 let point0_after_segment_edit = point_position(&scene_delta.new_graph, point0_id);
10809 let point1_after_segment_edit = point_position(&scene_delta.new_graph, point1_id);
10810
10811 let label_position = Point2d {
10812 x: Number {
10813 value: 3.0,
10814 units: NumericSuffix::Mm,
10815 },
10816 y: Number {
10817 value: 4.0,
10818 units: NumericSuffix::Mm,
10819 },
10820 };
10821 let (_, scene_delta) = frontend
10822 .edit_distance_constraint_label_position(
10823 &mock_ctx,
10824 version,
10825 sketch_id,
10826 constraint_id,
10827 label_position,
10828 vec![point0_id],
10829 )
10830 .await
10831 .unwrap();
10832
10833 assert_point_position_close(
10834 point_position(&scene_delta.new_graph, point0_id),
10835 point0_after_segment_edit,
10836 );
10837 assert_point_position_close(
10838 point_position(&scene_delta.new_graph, point1_id),
10839 point1_after_segment_edit,
10840 );
10841
10842 mock_ctx.close().await;
10843 }
10844
10845 #[tokio::test(flavor = "multi_thread")]
10846 async fn test_distance_point_line() {
10847 let initial_source = "\
10848sketch(on = XY) {
10849 point(at = [var 0, var 5])
10850 line(start = [var 0, var 0], end = [var 10, var 0])
10851}
10852";
10853
10854 let program = Program::parse(initial_source).unwrap().0.unwrap();
10855
10856 let mut frontend = FrontendState::new();
10857
10858 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10859 let mock_ctx = ExecutorContext::new_mock(None).await;
10860 let version = Version(0);
10861
10862 frontend.hack_set_program(&ctx, program).await.unwrap();
10863 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10864 let sketch_id = sketch_object.id;
10865 let sketch = expect_sketch(sketch_object);
10866 let point_id = *sketch.segments.first().unwrap();
10867 let line_id = *sketch
10868 .segments
10869 .iter()
10870 .find(|segment_id| {
10871 matches!(
10872 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
10873 Some(ObjectKind::Segment {
10874 segment: Segment::Line(_)
10875 })
10876 )
10877 })
10878 .unwrap();
10879
10880 let label_position = Point2d {
10881 x: Number {
10882 value: 10.0,
10883 units: NumericSuffix::Mm,
10884 },
10885 y: Number {
10886 value: 11.0,
10887 units: NumericSuffix::Mm,
10888 },
10889 };
10890 let constraint = Constraint::Distance(Distance {
10891 points: vec![point_id.into(), line_id.into()],
10892 distance: Number {
10893 value: 5.0,
10894 units: NumericSuffix::Mm,
10895 },
10896 label_position: Some(label_position.clone()),
10897 source: Default::default(),
10898 });
10899 let (src_delta, scene_delta) = frontend
10900 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10901 .await
10902 .unwrap();
10903 insta::assert_snapshot!("test_distance_point_line", src_delta.text.as_str());
10904 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10905 let sketch = expect_sketch(sketch_object);
10906 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10907 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10908 panic!("Expected constraint object");
10909 };
10910 let Constraint::Distance(distance) = constraint else {
10911 panic!("Expected distance constraint");
10912 };
10913 assert_eq!(distance.label_position, Some(label_position));
10914
10915 ctx.close().await;
10916 mock_ctx.close().await;
10917 }
10918
10919 #[tokio::test(flavor = "multi_thread")]
10920 async fn test_distance_point_arc() {
10921 let initial_source = "\
10922sketch(on = XY) {
10923 point(at = [var 0, var 8])
10924 arc(start = [var 5, var 0], end = [var 0, var 5], center = [var 0, var 0])
10925}
10926";
10927
10928 let program = Program::parse(initial_source).unwrap().0.unwrap();
10929
10930 let mut frontend = FrontendState::new();
10931
10932 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10933 let mock_ctx = ExecutorContext::new_mock(None).await;
10934 let version = Version(0);
10935
10936 frontend.hack_set_program(&ctx, program).await.unwrap();
10937 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10938 let sketch_id = sketch_object.id;
10939 let sketch = expect_sketch(sketch_object);
10940 let point_id = *sketch.segments.first().unwrap();
10941 let arc_id = *sketch
10942 .segments
10943 .iter()
10944 .find(|segment_id| {
10945 matches!(
10946 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
10947 Some(ObjectKind::Segment {
10948 segment: Segment::Arc(_)
10949 })
10950 )
10951 })
10952 .unwrap();
10953
10954 let constraint = Constraint::Distance(Distance {
10955 points: vec![point_id.into(), arc_id.into()],
10956 distance: Number {
10957 value: 3.0,
10958 units: NumericSuffix::Mm,
10959 },
10960 label_position: None,
10961 source: Default::default(),
10962 });
10963 let (src_delta, _scene_delta) = frontend
10964 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10965 .await
10966 .unwrap();
10967 insta::assert_snapshot!("test_distance_point_arc", src_delta.text.as_str());
10968
10969 ctx.close().await;
10970 mock_ctx.close().await;
10971 }
10972
10973 #[tokio::test(flavor = "multi_thread")]
10974 async fn test_distance_arc_origin() {
10975 let initial_source = "\
10976sketch001 = sketch(on = XY) {
10977 arc(start = [var -4.13mm, var -0.59mm], end = [var -3.47mm, var 3.38mm], center = [var -4.55mm, var 1.52mm])
10978}
10979";
10980
10981 let program = Program::parse(initial_source).unwrap().0.unwrap();
10982
10983 let mut frontend = FrontendState::new();
10984
10985 let mock_ctx = ExecutorContext::new_mock(None).await;
10986 let version = Version(0);
10987
10988 frontend.program = program.clone();
10989 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10990 frontend.update_state_after_exec(outcome, true);
10991 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10992 let sketch_id = sketch_object.id;
10993 let sketch = expect_sketch(sketch_object);
10994 let arc_id = *sketch
10995 .segments
10996 .iter()
10997 .find(|segment_id| {
10998 matches!(
10999 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11000 Some(ObjectKind::Segment {
11001 segment: Segment::Arc(_)
11002 })
11003 )
11004 })
11005 .unwrap();
11006
11007 let constraint = Constraint::Distance(Distance {
11008 points: vec![arc_id.into(), ConstraintSegment::ORIGIN],
11009 distance: Number {
11010 value: 3.0,
11011 units: NumericSuffix::Mm,
11012 },
11013 label_position: None,
11014 source: Default::default(),
11015 });
11016 let (src_delta, _scene_delta) = frontend
11017 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11018 .await
11019 .unwrap();
11020 insta::assert_snapshot!("test_distance_arc_origin", src_delta.text.as_str());
11021
11022 mock_ctx.close().await;
11023 }
11024
11025 #[tokio::test(flavor = "multi_thread")]
11026 async fn test_distance_line_origin() {
11027 let initial_source = "\
11028sketch(on = XY) {
11029 line(start = [var 5, var 0], end = [var 5, var 10])
11030}
11031";
11032
11033 let program = Program::parse(initial_source).unwrap().0.unwrap();
11034
11035 let mut frontend = FrontendState::new();
11036
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 line_id = *sketch
11047 .segments
11048 .iter()
11049 .find(|segment_id| {
11050 matches!(
11051 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11052 Some(ObjectKind::Segment {
11053 segment: Segment::Line(_)
11054 })
11055 )
11056 })
11057 .unwrap();
11058
11059 let constraint = Constraint::Distance(Distance {
11060 points: vec![ConstraintSegment::ORIGIN, line_id.into()],
11061 distance: Number {
11062 value: 5.0,
11063 units: NumericSuffix::Mm,
11064 },
11065 label_position: None,
11066 source: Default::default(),
11067 });
11068 let (src_delta, _scene_delta) = frontend
11069 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11070 .await
11071 .unwrap();
11072 insta::assert_snapshot!("test_distance_line_origin", src_delta.text.as_str());
11073
11074 mock_ctx.close().await;
11075 }
11076
11077 #[tokio::test(flavor = "multi_thread")]
11078 async fn test_distance_line_circle() {
11079 let initial_source = "\
11080sketch(on = XY) {
11081 line(start = [var -10, var 8], end = [var 10, var 8])
11082 circle(start = [var 5, var 0], center = [var 0, var 0])
11083}
11084";
11085
11086 let program = Program::parse(initial_source).unwrap().0.unwrap();
11087
11088 let mut frontend = FrontendState::new();
11089
11090 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11091 let mock_ctx = ExecutorContext::new_mock(None).await;
11092 let version = Version(0);
11093
11094 frontend.hack_set_program(&ctx, program).await.unwrap();
11095 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11096 let sketch_id = sketch_object.id;
11097 let sketch = expect_sketch(sketch_object);
11098 let line_id = *sketch
11099 .segments
11100 .iter()
11101 .find(|segment_id| {
11102 matches!(
11103 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11104 Some(ObjectKind::Segment {
11105 segment: Segment::Line(_)
11106 })
11107 )
11108 })
11109 .unwrap();
11110 let circle_id = *sketch
11111 .segments
11112 .iter()
11113 .find(|segment_id| {
11114 matches!(
11115 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11116 Some(ObjectKind::Segment {
11117 segment: Segment::Circle(_)
11118 })
11119 )
11120 })
11121 .unwrap();
11122
11123 let constraint = Constraint::Distance(Distance {
11124 points: vec![line_id.into(), circle_id.into()],
11125 distance: Number {
11126 value: 3.0,
11127 units: NumericSuffix::Mm,
11128 },
11129 label_position: None,
11130 source: Default::default(),
11131 });
11132 let (src_delta, _scene_delta) = frontend
11133 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11134 .await
11135 .unwrap();
11136 insta::assert_snapshot!("test_distance_line_circle", src_delta.text.as_str());
11137
11138 ctx.close().await;
11139 mock_ctx.close().await;
11140 }
11141
11142 #[tokio::test(flavor = "multi_thread")]
11143 async fn test_distance_circle_arc() {
11144 let initial_source = "\
11145sketch(on = XY) {
11146 circle(start = [var 5, var 0], center = [var 0, var 0])
11147 arc(start = [var 15, var 0], end = [var 10, var 5], center = [var 10, var 0])
11148}
11149";
11150
11151 let program = Program::parse(initial_source).unwrap().0.unwrap();
11152
11153 let mut frontend = FrontendState::new();
11154
11155 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11156 let mock_ctx = ExecutorContext::new_mock(None).await;
11157 let version = Version(0);
11158
11159 frontend.hack_set_program(&ctx, program).await.unwrap();
11160 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11161 let sketch_id = sketch_object.id;
11162 let sketch = expect_sketch(sketch_object);
11163 let circle_id = *sketch
11164 .segments
11165 .iter()
11166 .find(|segment_id| {
11167 matches!(
11168 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11169 Some(ObjectKind::Segment {
11170 segment: Segment::Circle(_)
11171 })
11172 )
11173 })
11174 .unwrap();
11175 let arc_id = *sketch
11176 .segments
11177 .iter()
11178 .find(|segment_id| {
11179 matches!(
11180 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11181 Some(ObjectKind::Segment {
11182 segment: Segment::Arc(_)
11183 })
11184 )
11185 })
11186 .unwrap();
11187
11188 let constraint = Constraint::Distance(Distance {
11189 points: vec![circle_id.into(), arc_id.into()],
11190 distance: Number {
11191 value: 3.0,
11192 units: NumericSuffix::Mm,
11193 },
11194 label_position: None,
11195 source: Default::default(),
11196 });
11197 let (src_delta, _scene_delta) = frontend
11198 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11199 .await
11200 .unwrap();
11201 insta::assert_snapshot!("test_distance_circle_arc", src_delta.text.as_str());
11202
11203 ctx.close().await;
11204 mock_ctx.close().await;
11205 }
11206
11207 #[tokio::test(flavor = "multi_thread")]
11208 async fn test_distance_parallel_lines() {
11209 let initial_source = "\
11210sketch(on = XY) {
11211 line(start = [var 0, var 0], end = [var 10, var 0])
11212 line(start = [var 0, var 5], end = [var 10, var 5])
11213}
11214";
11215
11216 let program = Program::parse(initial_source).unwrap().0.unwrap();
11217
11218 let mut frontend = FrontendState::new();
11219
11220 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11221 let mock_ctx = ExecutorContext::new_mock(None).await;
11222 let version = Version(0);
11223
11224 frontend.hack_set_program(&ctx, program).await.unwrap();
11225 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11226 let sketch_id = sketch_object.id;
11227 let sketch = expect_sketch(sketch_object);
11228 let line_ids = sketch
11229 .segments
11230 .iter()
11231 .copied()
11232 .filter(|segment_id| {
11233 matches!(
11234 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11235 Some(ObjectKind::Segment {
11236 segment: Segment::Line(_)
11237 })
11238 )
11239 })
11240 .collect::<Vec<_>>();
11241
11242 let constraint = Constraint::Distance(Distance {
11243 points: vec![line_ids[0].into(), line_ids[1].into()],
11244 distance: Number {
11245 value: 5.0,
11246 units: NumericSuffix::Mm,
11247 },
11248 label_position: None,
11249 source: Default::default(),
11250 });
11251 let (src_delta, _scene_delta) = frontend
11252 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11253 .await
11254 .unwrap();
11255 insta::assert_snapshot!("test_distance_parallel_lines", src_delta.text.as_str());
11256
11257 ctx.close().await;
11258 mock_ctx.close().await;
11259 }
11260
11261 #[tokio::test(flavor = "multi_thread")]
11262 async fn test_distance_non_parallel_lines_lowers_to_distance() {
11263 let initial_source = "\
11264sketch(on = XY) {
11265 line(start = [var 0, var 0], end = [var 10, var 0])
11266 line(start = [var 0, var 0], end = [var 0, var 10])
11267}
11268";
11269
11270 let program = Program::parse(initial_source).unwrap().0.unwrap();
11271
11272 let mut frontend = FrontendState::new();
11273
11274 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11275 let mock_ctx = ExecutorContext::new_mock(None).await;
11276 let version = Version(0);
11277
11278 frontend.hack_set_program(&ctx, program).await.unwrap();
11279 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11280 let sketch_id = sketch_object.id;
11281 let sketch = expect_sketch(sketch_object);
11282 let line_ids = sketch
11283 .segments
11284 .iter()
11285 .copied()
11286 .filter(|segment_id| {
11287 matches!(
11288 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11289 Some(ObjectKind::Segment {
11290 segment: Segment::Line(_)
11291 })
11292 )
11293 })
11294 .collect::<Vec<_>>();
11295
11296 let constraint = Constraint::Distance(Distance {
11297 points: vec![line_ids[0].into(), line_ids[1].into()],
11298 distance: Number {
11299 value: 5.0,
11300 units: NumericSuffix::Mm,
11301 },
11302 label_position: None,
11303 source: Default::default(),
11304 });
11305 let (src_delta, _scene_delta) = frontend
11306 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11307 .await
11308 .unwrap();
11309 insta::assert_snapshot!(
11310 "test_distance_non_parallel_lines_lowers_to_distance",
11311 src_delta.text.as_str()
11312 );
11313
11314 ctx.close().await;
11315 mock_ctx.close().await;
11316 }
11317
11318 #[tokio::test(flavor = "multi_thread")]
11319 async fn test_horizontal_distance_two_points() {
11320 let initial_source = "\
11321sketch(on = XY) {
11322 point(at = [var 1, var 2])
11323 point(at = [var 3, var 4])
11324}
11325";
11326
11327 let program = Program::parse(initial_source).unwrap().0.unwrap();
11328
11329 let mut frontend = FrontendState::new();
11330
11331 let mock_ctx = ExecutorContext::new_mock(None).await;
11332 let version = Version(0);
11333
11334 frontend.program = program.clone();
11335 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11336 frontend.update_state_after_exec(outcome, true);
11337 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11338 let sketch_id = sketch_object.id;
11339 let sketch = expect_sketch(sketch_object);
11340 let point0_id = *sketch.segments.first().unwrap();
11341 let point1_id = *sketch.segments.get(1).unwrap();
11342 let label_position = Point2d {
11343 x: Number {
11344 value: 10.0,
11345 units: NumericSuffix::Mm,
11346 },
11347 y: Number {
11348 value: 11.0,
11349 units: NumericSuffix::Mm,
11350 },
11351 };
11352
11353 let constraint = Constraint::HorizontalDistance(Distance {
11354 points: vec![point0_id.into(), point1_id.into()],
11355 distance: Number {
11356 value: 2.0,
11357 units: NumericSuffix::Mm,
11358 },
11359 label_position: Some(label_position.clone()),
11360 source: Default::default(),
11361 });
11362 let (src_delta, scene_delta) = frontend
11363 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11364 .await
11365 .unwrap();
11366 insta::assert_snapshot!("test_horizontal_distance_two_points", src_delta.text.as_str());
11367 assert_eq!(
11368 scene_delta.new_graph.objects.len(),
11369 5,
11370 "{:#?}",
11371 scene_delta.new_graph.objects
11372 );
11373 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
11374 let sketch = expect_sketch(sketch_object);
11375 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
11376 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11377 panic!("Expected constraint object");
11378 };
11379 let Constraint::HorizontalDistance(distance) = constraint else {
11380 panic!("Expected horizontal distance constraint");
11381 };
11382 assert_eq!(distance.label_position, Some(label_position));
11383
11384 mock_ctx.close().await;
11385 }
11386
11387 #[tokio::test(flavor = "multi_thread")]
11388 async fn test_radius_single_arc_segment() {
11389 let initial_source = "\
11390sketch(on = XY) {
11391 arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
11392}
11393";
11394
11395 let program = Program::parse(initial_source).unwrap().0.unwrap();
11396
11397 let mut frontend = FrontendState::new();
11398
11399 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11400 let mock_ctx = ExecutorContext::new_mock(None).await;
11401 let version = Version(0);
11402
11403 frontend.hack_set_program(&ctx, program).await.unwrap();
11404 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11405 let sketch_id = sketch_object.id;
11406 let sketch = expect_sketch(sketch_object);
11407 let arc_id = sketch
11409 .segments
11410 .iter()
11411 .find(|&seg_id| {
11412 let obj = frontend.scene_graph.objects.get(seg_id.0);
11413 matches!(
11414 obj.map(|o| &o.kind),
11415 Some(ObjectKind::Segment {
11416 segment: Segment::Arc(_)
11417 })
11418 )
11419 })
11420 .unwrap();
11421
11422 let constraint = Constraint::Radius(Radius {
11423 arc: *arc_id,
11424 radius: Number {
11425 value: 5.0,
11426 units: NumericSuffix::Mm,
11427 },
11428 label_position: None,
11429 source: Default::default(),
11430 });
11431 let (src_delta, scene_delta) = frontend
11432 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11433 .await
11434 .unwrap();
11435 insta::assert_snapshot!("test_radius_single_arc_segment", src_delta.text.as_str());
11436 assert_eq!(
11437 scene_delta.new_graph.objects.len(),
11438 7, "{:#?}",
11440 scene_delta.new_graph.objects
11441 );
11442
11443 ctx.close().await;
11444 mock_ctx.close().await;
11445 }
11446
11447 #[tokio::test(flavor = "multi_thread")]
11448 async fn test_radius_single_arc_segment_with_label_position() {
11449 let initial_source = "\
11450sketch(on = XY) {
11451 arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
11452}
11453";
11454
11455 let program = Program::parse(initial_source).unwrap().0.unwrap();
11456 let mut frontend = FrontendState::new();
11457 let mock_ctx = ExecutorContext::new_mock(None).await;
11458 let version = Version(0);
11459
11460 frontend.program = program.clone();
11461 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11462 frontend.update_state_after_exec(outcome, true);
11463 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11464 let sketch_id = sketch_object.id;
11465 let sketch = expect_sketch(sketch_object);
11466 let arc_id = sketch
11467 .segments
11468 .iter()
11469 .find(|&seg_id| {
11470 let obj = frontend.scene_graph.objects.get(seg_id.0);
11471 matches!(
11472 obj.map(|o| &o.kind),
11473 Some(ObjectKind::Segment {
11474 segment: Segment::Arc(_)
11475 })
11476 )
11477 })
11478 .unwrap();
11479
11480 let label_position = Point2d {
11481 x: Number {
11482 value: 10.0,
11483 units: NumericSuffix::Mm,
11484 },
11485 y: Number {
11486 value: 11.0,
11487 units: NumericSuffix::Mm,
11488 },
11489 };
11490 let constraint = Constraint::Radius(Radius {
11491 arc: *arc_id,
11492 radius: Number {
11493 value: 5.0,
11494 units: NumericSuffix::Mm,
11495 },
11496 label_position: Some(label_position.clone()),
11497 source: Default::default(),
11498 });
11499 let (src_delta, scene_delta) = frontend
11500 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11501 .await
11502 .unwrap();
11503 insta::assert_snapshot!(
11504 "test_radius_single_arc_segment_with_label_position",
11505 src_delta.text.as_str()
11506 );
11507
11508 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
11509 let sketch = expect_sketch(sketch_object);
11510 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
11511 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11512 panic!("Expected constraint object");
11513 };
11514 let Constraint::Radius(radius) = constraint else {
11515 panic!("Expected radius constraint");
11516 };
11517 assert_eq!(radius.label_position, Some(label_position));
11518
11519 mock_ctx.close().await;
11520 }
11521
11522 #[tokio::test(flavor = "multi_thread")]
11523 async fn test_edit_radius_constraint_label_position() {
11524 let initial_source = "\
11525sketch(on = XY) {
11526 arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
11527 radius(arc1) == 5mm
11528}
11529";
11530
11531 let program = Program::parse(initial_source).unwrap().0.unwrap();
11532 let mut frontend = FrontendState::new();
11533 let mock_ctx = ExecutorContext::new_mock(None).await;
11534 let version = Version(0);
11535
11536 frontend.program = program.clone();
11537 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11538 frontend.update_state_after_exec(outcome, true);
11539 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11540 let sketch_id = sketch_object.id;
11541 let sketch = expect_sketch(sketch_object);
11542 let constraint_id = sketch.constraints[0];
11543 let label_position = Point2d {
11544 x: Number {
11545 value: 10.0,
11546 units: NumericSuffix::Mm,
11547 },
11548 y: Number {
11549 value: 11.0,
11550 units: NumericSuffix::Mm,
11551 },
11552 };
11553
11554 let (src_delta, scene_delta) = frontend
11555 .edit_distance_constraint_label_position(
11556 &mock_ctx,
11557 version,
11558 sketch_id,
11559 constraint_id,
11560 label_position.clone(),
11561 vec![],
11562 )
11563 .await
11564 .unwrap();
11565 insta::assert_snapshot!("test_edit_radius_constraint_label_position", src_delta.text.as_str());
11566
11567 let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
11568 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11569 panic!("Expected constraint object");
11570 };
11571 let Constraint::Radius(radius) = constraint else {
11572 panic!("Expected radius constraint");
11573 };
11574 assert_eq!(radius.label_position, Some(label_position));
11575
11576 mock_ctx.close().await;
11577 }
11578
11579 #[tokio::test(flavor = "multi_thread")]
11580 async fn test_vertical_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::VerticalDistance(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_vertical_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::VerticalDistance(distance) = constraint else {
11641 panic!("Expected vertical 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_add_fixed_standalone_point() {
11650 let initial_source = "\
11651sketch(on = XY) {
11652 point(at = [var 1, var 2])
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 point_id = *sketch.segments.first().unwrap();
11669
11670 let (src_delta, scene_delta) = frontend
11671 .add_constraint(
11672 &mock_ctx,
11673 version,
11674 sketch_id,
11675 Constraint::Fixed(Fixed {
11676 points: vec![FixedPoint {
11677 point: point_id,
11678 position: Point2d {
11679 x: Number {
11680 value: 2.0,
11681 units: NumericSuffix::Mm,
11682 },
11683 y: Number {
11684 value: 3.0,
11685 units: NumericSuffix::Mm,
11686 },
11687 },
11688 }],
11689 }),
11690 )
11691 .await
11692 .unwrap();
11693 insta::assert_snapshot!("test_add_fixed_standalone_point", src_delta.text.as_str());
11694 assert_eq!(
11695 scene_delta.new_graph.objects.len(),
11696 4,
11697 "{:#?}",
11698 scene_delta.new_graph.objects
11699 );
11700
11701 ctx.close().await;
11702 mock_ctx.close().await;
11703 }
11704
11705 #[tokio::test(flavor = "multi_thread")]
11706 async fn test_add_fixed_multiple_points() {
11707 let initial_source = "\
11708sketch(on = XY) {
11709 point(at = [var 1, var 2])
11710 point(at = [var 3, var 4])
11711}
11712";
11713
11714 let program = Program::parse(initial_source).unwrap().0.unwrap();
11715
11716 let mut frontend = FrontendState::new();
11717
11718 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11719 let mock_ctx = ExecutorContext::new_mock(None).await;
11720 let version = Version(0);
11721
11722 frontend.hack_set_program(&ctx, program).await.unwrap();
11723 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11724 let sketch_id = sketch_object.id;
11725 let sketch = expect_sketch(sketch_object);
11726 let point0_id = *sketch.segments.first().unwrap();
11727 let point1_id = *sketch.segments.get(1).unwrap();
11728
11729 let (src_delta, scene_delta) = frontend
11730 .add_constraint(
11731 &mock_ctx,
11732 version,
11733 sketch_id,
11734 Constraint::Fixed(Fixed {
11735 points: vec![
11736 FixedPoint {
11737 point: point0_id,
11738 position: Point2d {
11739 x: Number {
11740 value: 2.0,
11741 units: NumericSuffix::Mm,
11742 },
11743 y: Number {
11744 value: 3.0,
11745 units: NumericSuffix::Mm,
11746 },
11747 },
11748 },
11749 FixedPoint {
11750 point: point1_id,
11751 position: Point2d {
11752 x: Number {
11753 value: 4.0,
11754 units: NumericSuffix::Mm,
11755 },
11756 y: Number {
11757 value: 5.0,
11758 units: NumericSuffix::Mm,
11759 },
11760 },
11761 },
11762 ],
11763 }),
11764 )
11765 .await
11766 .unwrap();
11767 insta::assert_snapshot!("test_add_fixed_multiple_points", src_delta.text.as_str());
11768 assert_eq!(
11769 scene_delta.new_graph.objects.len(),
11770 6,
11771 "{:#?}",
11772 scene_delta.new_graph.objects
11773 );
11774
11775 ctx.close().await;
11776 mock_ctx.close().await;
11777 }
11778
11779 #[tokio::test(flavor = "multi_thread")]
11780 async fn test_add_fixed_owned_point() {
11781 let initial_source = "\
11782sketch(on = XY) {
11783 line(start = [var 1, var 2], end = [var 3, var 4])
11784}
11785";
11786
11787 let program = Program::parse(initial_source).unwrap().0.unwrap();
11788
11789 let mut frontend = FrontendState::new();
11790
11791 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11792 let mock_ctx = ExecutorContext::new_mock(None).await;
11793 let version = Version(0);
11794
11795 frontend.hack_set_program(&ctx, program).await.unwrap();
11796 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11797 let sketch_id = sketch_object.id;
11798 let sketch = expect_sketch(sketch_object);
11799 let line_start_id = *sketch.segments.first().unwrap();
11800
11801 let (src_delta, scene_delta) = frontend
11802 .add_constraint(
11803 &mock_ctx,
11804 version,
11805 sketch_id,
11806 Constraint::Fixed(Fixed {
11807 points: vec![FixedPoint {
11808 point: line_start_id,
11809 position: Point2d {
11810 x: Number {
11811 value: 2.0,
11812 units: NumericSuffix::Mm,
11813 },
11814 y: Number {
11815 value: 3.0,
11816 units: NumericSuffix::Mm,
11817 },
11818 },
11819 }],
11820 }),
11821 )
11822 .await
11823 .unwrap();
11824 insta::assert_snapshot!("test_add_fixed_owned_point", src_delta.text.as_str());
11825 assert_eq!(
11826 scene_delta.new_graph.objects.len(),
11827 6,
11828 "{:#?}",
11829 scene_delta.new_graph.objects
11830 );
11831
11832 ctx.close().await;
11833 mock_ctx.close().await;
11834 }
11835
11836 #[tokio::test(flavor = "multi_thread")]
11837 async fn test_radius_error_cases() {
11838 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11839 let mock_ctx = ExecutorContext::new_mock(None).await;
11840 let version = Version(0);
11841
11842 let initial_source_point = "\
11844sketch(on = XY) {
11845 point(at = [var 1, var 2])
11846}
11847";
11848 let program_point = Program::parse(initial_source_point).unwrap().0.unwrap();
11849 let mut frontend_point = FrontendState::new();
11850 frontend_point.hack_set_program(&ctx, program_point).await.unwrap();
11851 let sketch_object_point = find_first_sketch_object(&frontend_point.scene_graph).unwrap();
11852 let sketch_id_point = sketch_object_point.id;
11853 let sketch_point = expect_sketch(sketch_object_point);
11854 let point_id = *sketch_point.segments.first().unwrap();
11855
11856 let constraint_point = Constraint::Radius(Radius {
11857 arc: point_id,
11858 radius: Number {
11859 value: 5.0,
11860 units: NumericSuffix::Mm,
11861 },
11862 label_position: None,
11863 source: Default::default(),
11864 });
11865 let result_point = frontend_point
11866 .add_constraint(&mock_ctx, version, sketch_id_point, constraint_point)
11867 .await;
11868 assert!(result_point.is_err(), "Single point should error for radius");
11869
11870 let initial_source_line = "\
11872sketch(on = XY) {
11873 line(start = [var 1, var 2], end = [var 3, var 4])
11874}
11875";
11876 let program_line = Program::parse(initial_source_line).unwrap().0.unwrap();
11877 let mut frontend_line = FrontendState::new();
11878 frontend_line.hack_set_program(&ctx, program_line).await.unwrap();
11879 let sketch_object_line = find_first_sketch_object(&frontend_line.scene_graph).unwrap();
11880 let sketch_id_line = sketch_object_line.id;
11881 let sketch_line = expect_sketch(sketch_object_line);
11882 let line_id = *sketch_line.segments.first().unwrap();
11883
11884 let constraint_line = Constraint::Radius(Radius {
11885 arc: line_id,
11886 radius: Number {
11887 value: 5.0,
11888 units: NumericSuffix::Mm,
11889 },
11890 label_position: None,
11891 source: Default::default(),
11892 });
11893 let result_line = frontend_line
11894 .add_constraint(&mock_ctx, version, sketch_id_line, constraint_line)
11895 .await;
11896 assert!(result_line.is_err(), "Single line segment should error for radius");
11897
11898 ctx.close().await;
11899 mock_ctx.close().await;
11900 }
11901
11902 #[tokio::test(flavor = "multi_thread")]
11903 async fn test_diameter_single_arc_segment() {
11904 let initial_source = "\
11905sketch(on = XY) {
11906 arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
11907}
11908";
11909
11910 let program = Program::parse(initial_source).unwrap().0.unwrap();
11911
11912 let mut frontend = FrontendState::new();
11913
11914 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11915 let mock_ctx = ExecutorContext::new_mock(None).await;
11916 let version = Version(0);
11917
11918 frontend.hack_set_program(&ctx, program).await.unwrap();
11919 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11920 let sketch_id = sketch_object.id;
11921 let sketch = expect_sketch(sketch_object);
11922 let arc_id = sketch
11924 .segments
11925 .iter()
11926 .find(|&seg_id| {
11927 let obj = frontend.scene_graph.objects.get(seg_id.0);
11928 matches!(
11929 obj.map(|o| &o.kind),
11930 Some(ObjectKind::Segment {
11931 segment: Segment::Arc(_)
11932 })
11933 )
11934 })
11935 .unwrap();
11936
11937 let constraint = Constraint::Diameter(Diameter {
11938 arc: *arc_id,
11939 diameter: Number {
11940 value: 10.0,
11941 units: NumericSuffix::Mm,
11942 },
11943 label_position: None,
11944 source: Default::default(),
11945 });
11946 let (src_delta, scene_delta) = frontend
11947 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11948 .await
11949 .unwrap();
11950 insta::assert_snapshot!("test_diameter_single_arc_segment", src_delta.text.as_str());
11951 assert_eq!(
11952 scene_delta.new_graph.objects.len(),
11953 7, "{:#?}",
11955 scene_delta.new_graph.objects
11956 );
11957
11958 ctx.close().await;
11959 mock_ctx.close().await;
11960 }
11961
11962 #[tokio::test(flavor = "multi_thread")]
11963 async fn test_diameter_single_arc_segment_with_label_position() {
11964 let initial_source = "\
11965sketch(on = XY) {
11966 arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
11967}
11968";
11969
11970 let program = Program::parse(initial_source).unwrap().0.unwrap();
11971 let mut frontend = FrontendState::new();
11972 let mock_ctx = ExecutorContext::new_mock(None).await;
11973 let version = Version(0);
11974
11975 frontend.program = program.clone();
11976 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11977 frontend.update_state_after_exec(outcome, true);
11978 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11979 let sketch_id = sketch_object.id;
11980 let sketch = expect_sketch(sketch_object);
11981 let arc_id = sketch
11982 .segments
11983 .iter()
11984 .find(|&seg_id| {
11985 let obj = frontend.scene_graph.objects.get(seg_id.0);
11986 matches!(
11987 obj.map(|o| &o.kind),
11988 Some(ObjectKind::Segment {
11989 segment: Segment::Arc(_)
11990 })
11991 )
11992 })
11993 .unwrap();
11994
11995 let label_position = Point2d {
11996 x: Number {
11997 value: 10.0,
11998 units: NumericSuffix::Mm,
11999 },
12000 y: Number {
12001 value: 11.0,
12002 units: NumericSuffix::Mm,
12003 },
12004 };
12005 let constraint = Constraint::Diameter(Diameter {
12006 arc: *arc_id,
12007 diameter: Number {
12008 value: 10.0,
12009 units: NumericSuffix::Mm,
12010 },
12011 label_position: Some(label_position.clone()),
12012 source: Default::default(),
12013 });
12014 let (src_delta, scene_delta) = frontend
12015 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12016 .await
12017 .unwrap();
12018 insta::assert_snapshot!(
12019 "test_diameter_single_arc_segment_with_label_position",
12020 src_delta.text.as_str()
12021 );
12022
12023 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
12024 let sketch = expect_sketch(sketch_object);
12025 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
12026 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12027 panic!("Expected constraint object");
12028 };
12029 let Constraint::Diameter(diameter) = constraint else {
12030 panic!("Expected diameter constraint");
12031 };
12032 assert_eq!(diameter.label_position, Some(label_position));
12033
12034 mock_ctx.close().await;
12035 }
12036
12037 #[tokio::test(flavor = "multi_thread")]
12038 async fn test_edit_diameter_constraint_label_position() {
12039 let initial_source = "\
12040sketch(on = XY) {
12041 arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
12042 diameter(arc1) == 10mm
12043}
12044";
12045
12046 let program = Program::parse(initial_source).unwrap().0.unwrap();
12047 let mut frontend = FrontendState::new();
12048 let mock_ctx = ExecutorContext::new_mock(None).await;
12049 let version = Version(0);
12050
12051 frontend.program = program.clone();
12052 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12053 frontend.update_state_after_exec(outcome, true);
12054 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12055 let sketch_id = sketch_object.id;
12056 let sketch = expect_sketch(sketch_object);
12057 let constraint_id = sketch.constraints[0];
12058 let label_position = Point2d {
12059 x: Number {
12060 value: 10.0,
12061 units: NumericSuffix::Mm,
12062 },
12063 y: Number {
12064 value: 11.0,
12065 units: NumericSuffix::Mm,
12066 },
12067 };
12068
12069 let (src_delta, scene_delta) = frontend
12070 .edit_distance_constraint_label_position(
12071 &mock_ctx,
12072 version,
12073 sketch_id,
12074 constraint_id,
12075 label_position.clone(),
12076 vec![],
12077 )
12078 .await
12079 .unwrap();
12080 insta::assert_snapshot!("test_edit_diameter_constraint_label_position", src_delta.text.as_str());
12081
12082 let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
12083 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12084 panic!("Expected constraint object");
12085 };
12086 let Constraint::Diameter(diameter) = constraint else {
12087 panic!("Expected diameter constraint");
12088 };
12089 assert_eq!(diameter.label_position, Some(label_position));
12090
12091 mock_ctx.close().await;
12092 }
12093
12094 #[tokio::test(flavor = "multi_thread")]
12095 async fn test_diameter_error_cases() {
12096 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12097 let mock_ctx = ExecutorContext::new_mock(None).await;
12098 let version = Version(0);
12099
12100 let initial_source_point = "\
12102sketch(on = XY) {
12103 point(at = [var 1, var 2])
12104}
12105";
12106 let program_point = Program::parse(initial_source_point).unwrap().0.unwrap();
12107 let mut frontend_point = FrontendState::new();
12108 frontend_point.hack_set_program(&ctx, program_point).await.unwrap();
12109 let sketch_object_point = find_first_sketch_object(&frontend_point.scene_graph).unwrap();
12110 let sketch_id_point = sketch_object_point.id;
12111 let sketch_point = expect_sketch(sketch_object_point);
12112 let point_id = *sketch_point.segments.first().unwrap();
12113
12114 let constraint_point = Constraint::Diameter(Diameter {
12115 arc: point_id,
12116 diameter: Number {
12117 value: 10.0,
12118 units: NumericSuffix::Mm,
12119 },
12120 label_position: None,
12121 source: Default::default(),
12122 });
12123 let result_point = frontend_point
12124 .add_constraint(&mock_ctx, version, sketch_id_point, constraint_point)
12125 .await;
12126 assert!(result_point.is_err(), "Single point should error for diameter");
12127
12128 let initial_source_line = "\
12130sketch(on = XY) {
12131 line(start = [var 1, var 2], end = [var 3, var 4])
12132}
12133";
12134 let program_line = Program::parse(initial_source_line).unwrap().0.unwrap();
12135 let mut frontend_line = FrontendState::new();
12136 frontend_line.hack_set_program(&ctx, program_line).await.unwrap();
12137 let sketch_object_line = find_first_sketch_object(&frontend_line.scene_graph).unwrap();
12138 let sketch_id_line = sketch_object_line.id;
12139 let sketch_line = expect_sketch(sketch_object_line);
12140 let line_id = *sketch_line.segments.first().unwrap();
12141
12142 let constraint_line = Constraint::Diameter(Diameter {
12143 arc: line_id,
12144 diameter: Number {
12145 value: 10.0,
12146 units: NumericSuffix::Mm,
12147 },
12148 label_position: None,
12149 source: Default::default(),
12150 });
12151 let result_line = frontend_line
12152 .add_constraint(&mock_ctx, version, sketch_id_line, constraint_line)
12153 .await;
12154 assert!(result_line.is_err(), "Single line segment should error for diameter");
12155
12156 ctx.close().await;
12157 mock_ctx.close().await;
12158 }
12159
12160 #[tokio::test(flavor = "multi_thread")]
12161 async fn test_line_horizontal() {
12162 let initial_source = "\
12163sketch(on = XY) {
12164 line(start = [var 1, var 2], end = [var 3, var 4])
12165}
12166";
12167
12168 let program = Program::parse(initial_source).unwrap().0.unwrap();
12169
12170 let mut frontend = FrontendState::new();
12171
12172 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12173 let mock_ctx = ExecutorContext::new_mock(None).await;
12174 let version = Version(0);
12175
12176 frontend.hack_set_program(&ctx, program).await.unwrap();
12177 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12178 let sketch_id = sketch_object.id;
12179 let sketch = expect_sketch(sketch_object);
12180 let line1_id = *sketch.segments.get(2).unwrap();
12181
12182 let constraint = Constraint::Horizontal(Horizontal::Line { line: line1_id });
12183 let (src_delta, scene_delta) = frontend
12184 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12185 .await
12186 .unwrap();
12187 insta::assert_snapshot!("test_line_horizontal", src_delta.text.as_str());
12188 assert_eq!(
12189 scene_delta.new_graph.objects.len(),
12190 6,
12191 "{:#?}",
12192 scene_delta.new_graph.objects
12193 );
12194
12195 ctx.close().await;
12196 mock_ctx.close().await;
12197 }
12198
12199 #[tokio::test(flavor = "multi_thread")]
12200 async fn test_control_point_spline_edge_horizontal() {
12201 let initial_source = "\
12202@settings(experimentalFeatures = allow)
12203splineSketch = sketch(on = XY) {
12204 controlPointSpline1 = controlPointSpline(points = [
12205 [var 0mm, var 0mm],
12206 [var 10mm, var 20mm],
12207 [var 20mm, var 0mm],
12208 ])
12209}
12210";
12211
12212 let program = Program::parse(initial_source).unwrap().0.unwrap();
12213
12214 let mut frontend = FrontendState::new();
12215
12216 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12217 let mock_ctx = ExecutorContext::new_mock(None).await;
12218 let version = Version(0);
12219
12220 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
12221 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12222 let sketch_id = sketch_object.id;
12223 let sketch = expect_sketch(sketch_object);
12224 let spline_id = sketch
12225 .segments
12226 .iter()
12227 .copied()
12228 .find(|seg_id| {
12229 matches!(
12230 &frontend.scene_graph.objects[seg_id.0].kind,
12231 ObjectKind::Segment {
12232 segment: Segment::ControlPointSpline(_)
12233 }
12234 )
12235 })
12236 .expect("Expected a control point spline segment in sketch");
12237 let edge_id = frontend
12238 .scene_graph
12239 .objects
12240 .iter()
12241 .find_map(|obj| match &obj.kind {
12242 ObjectKind::Segment {
12243 segment: Segment::Line(line),
12244 } if line.owner == Some(spline_id) => Some(obj.id),
12245 _ => None,
12246 })
12247 .expect("Expected an owned control-polygon edge");
12248
12249 let constraint = Constraint::Horizontal(Horizontal::Line { line: edge_id });
12250 let (src_delta, _) = frontend
12251 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12252 .await
12253 .unwrap();
12254 assert!(
12255 src_delta.text.contains("horizontal(controlPointSpline1.edges[0])"),
12256 "Expected horizontal constraint on spline edge, got: {}",
12257 src_delta.text
12258 );
12259
12260 ctx.close().await;
12261 mock_ctx.close().await;
12262 }
12263
12264 #[tokio::test(flavor = "multi_thread")]
12265 async fn test_control_point_spline_edge_angle() {
12266 let initial_source = "\
12267@settings(experimentalFeatures = allow)
12268splineSketch = sketch(on = XY) {
12269 controlPointSpline1 = controlPointSpline(points = [
12270 [var 0mm, var 0mm],
12271 [var 10mm, var 20mm],
12272 [var 20mm, var 0mm],
12273 ])
12274
12275 line1 = line(start = [var 40mm, var 0mm], end = [var 60mm, var 10mm])
12276}
12277";
12278
12279 let program = Program::parse(initial_source).unwrap().0.unwrap();
12280
12281 let mut frontend = FrontendState::new();
12282
12283 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12284 let mock_ctx = ExecutorContext::new_mock(None).await;
12285 let version = Version(0);
12286
12287 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
12288 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12289 let sketch_id = sketch_object.id;
12290 let sketch = expect_sketch(sketch_object);
12291 let spline_id = sketch
12292 .segments
12293 .iter()
12294 .copied()
12295 .find(|seg_id| {
12296 matches!(
12297 &frontend.scene_graph.objects[seg_id.0].kind,
12298 ObjectKind::Segment {
12299 segment: Segment::ControlPointSpline(_)
12300 }
12301 )
12302 })
12303 .expect("Expected a control point spline segment in sketch");
12304 let edge_id = frontend
12305 .scene_graph
12306 .objects
12307 .iter()
12308 .find_map(|obj| match &obj.kind {
12309 ObjectKind::Segment {
12310 segment: Segment::Line(line),
12311 } if line.owner == Some(spline_id) => Some(obj.id),
12312 _ => None,
12313 })
12314 .expect("Expected an owned control-polygon edge");
12315 let line1_id = frontend
12316 .scene_graph
12317 .objects
12318 .iter()
12319 .find_map(|obj| match &obj.kind {
12320 ObjectKind::Segment {
12321 segment: Segment::Line(line),
12322 } if line.owner.is_none() && obj.label == "line1" => Some(obj.id),
12323 _ => None,
12324 })
12325 .or_else(|| {
12326 sketch.segments.iter().copied().find(|seg_id| {
12327 matches!(
12328 &frontend.scene_graph.objects[seg_id.0].kind,
12329 ObjectKind::Segment {
12330 segment: Segment::Line(line),
12331 } if line.owner.is_none()
12332 )
12333 })
12334 })
12335 .expect("Expected a standalone line segment in sketch");
12336
12337 let constraint = Constraint::Angle(Angle {
12338 lines: vec![line1_id, edge_id],
12339 angle: Number {
12340 value: 30.0,
12341 units: NumericSuffix::Deg,
12342 },
12343 source: Default::default(),
12344 });
12345 let (src_delta, _) = frontend
12346 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12347 .await
12348 .unwrap();
12349 assert!(
12350 src_delta
12351 .text
12352 .contains("angle([line1, controlPointSpline1.edges[0]]) == 30deg"),
12353 "Expected angle constraint on spline edge, got: {}",
12354 src_delta.text
12355 );
12356
12357 ctx.close().await;
12358 mock_ctx.close().await;
12359 }
12360
12361 #[tokio::test(flavor = "multi_thread")]
12362 async fn test_ui_scene_graph_hides_same_spline_coincident_constraints() {
12363 let initial_source = "\
12364@settings(experimentalFeatures = allow)
12365splineSketch = sketch(on = XY) {
12366 spline1 = controlPointSpline(points = [
12367 [var 0mm, var 0mm],
12368 [var 10mm, var 20mm],
12369 [var 20mm, var 0mm],
12370 ])
12371 line1 = line(start = [var 0mm, var 0mm], end = [var -10mm, var 0mm])
12372 coincident([spline1.controls[1], spline1.edges[0]])
12373 coincident([spline1.controls[0], line1])
12374}
12375";
12376
12377 let program = Program::parse(initial_source).unwrap().0.unwrap();
12378
12379 let mut frontend = FrontendState::new();
12380
12381 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12382 let mock_ctx = ExecutorContext::new_mock(None).await;
12383
12384 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
12385
12386 let ui_scene_graph = frontend.scene_graph_for_ui();
12387 let sketch_object = find_first_sketch_object(&ui_scene_graph).unwrap();
12388 let sketch = expect_sketch(sketch_object);
12389
12390 assert_eq!(
12391 sketch.constraints.len(),
12392 1,
12393 "Expected only the external coincident constraint to remain visible in the UI scene graph"
12394 );
12395
12396 let visible_constraints = ui_scene_graph
12397 .objects
12398 .iter()
12399 .filter_map(|object| match &object.kind {
12400 ObjectKind::Constraint {
12401 constraint: Constraint::Coincident(coincident),
12402 } => Some(coincident.clone()),
12403 _ => None,
12404 })
12405 .collect::<Vec<_>>();
12406
12407 assert_eq!(
12408 visible_constraints.len(),
12409 1,
12410 "Expected only one coincident constraint object in the UI scene graph"
12411 );
12412 assert_eq!(
12413 visible_constraints[0].get_segments().len(),
12414 2,
12415 "Expected the remaining visible coincident constraint to reference two segments"
12416 );
12417
12418 ctx.close().await;
12419 mock_ctx.close().await;
12420 }
12421
12422 #[tokio::test(flavor = "multi_thread")]
12423 async fn test_edit_control_point_spline_can_append_control_point() {
12424 let initial_source = "\
12425@settings(experimentalFeatures = allow)
12426splineSketch = sketch(on = XY) {
12427 controlPointSpline(points = [
12428 [var 0mm, var 0mm],
12429 [var 10mm, var 20mm],
12430 [var 20mm, var 0mm],
12431 ])
12432}
12433";
12434
12435 let program = Program::parse(initial_source).unwrap().0.unwrap();
12436
12437 let mut frontend = FrontendState::new();
12438
12439 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12440 let mock_ctx = ExecutorContext::new_mock(None).await;
12441 let version = Version(0);
12442
12443 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
12444 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12445 let sketch_id = sketch_object.id;
12446 let sketch = expect_sketch(sketch_object);
12447 let spline_id = sketch
12448 .segments
12449 .iter()
12450 .copied()
12451 .find(|seg_id| {
12452 matches!(
12453 &frontend.scene_graph.objects[seg_id.0].kind,
12454 ObjectKind::Segment {
12455 segment: Segment::ControlPointSpline(_)
12456 }
12457 )
12458 })
12459 .expect("Expected a control point spline segment in sketch");
12460
12461 let ctor = ControlPointSplineCtor {
12462 points: vec![
12463 Point2d {
12464 x: Expr::Var(Number {
12465 value: 0.0,
12466 units: NumericSuffix::Mm,
12467 }),
12468 y: Expr::Var(Number {
12469 value: 0.0,
12470 units: NumericSuffix::Mm,
12471 }),
12472 },
12473 Point2d {
12474 x: Expr::Var(Number {
12475 value: 10.0,
12476 units: NumericSuffix::Mm,
12477 }),
12478 y: Expr::Var(Number {
12479 value: 20.0,
12480 units: NumericSuffix::Mm,
12481 }),
12482 },
12483 Point2d {
12484 x: Expr::Var(Number {
12485 value: 20.0,
12486 units: NumericSuffix::Mm,
12487 }),
12488 y: Expr::Var(Number {
12489 value: 0.0,
12490 units: NumericSuffix::Mm,
12491 }),
12492 },
12493 Point2d {
12494 x: Expr::Var(Number {
12495 value: 30.0,
12496 units: NumericSuffix::Mm,
12497 }),
12498 y: Expr::Var(Number {
12499 value: 10.0,
12500 units: NumericSuffix::Mm,
12501 }),
12502 },
12503 ],
12504 construction: None,
12505 };
12506
12507 let segments = vec![ExistingSegmentCtor {
12508 id: spline_id,
12509 ctor: SegmentCtor::ControlPointSpline(ctor),
12510 }];
12511 let (src_delta, scene_delta) = frontend
12512 .edit_segments(&mock_ctx, version, sketch_id, segments)
12513 .await
12514 .unwrap();
12515
12516 assert!(
12517 src_delta.text.contains("[var 30mm, var 10mm]"),
12518 "Expected appended spline control point in source, got: {}",
12519 src_delta.text
12520 );
12521
12522 assert!(
12523 scene_delta.invalidates_ids,
12524 "Expected appending a spline control point to invalidate ids"
12525 );
12526 let updated_spline = scene_delta
12527 .new_graph
12528 .objects
12529 .iter()
12530 .find_map(|obj| match &obj.kind {
12531 ObjectKind::Segment {
12532 segment: Segment::ControlPointSpline(updated_spline),
12533 } if updated_spline.controls.len() == 4 => Some(updated_spline),
12534 _ => None,
12535 })
12536 .expect("Expected edited scene graph to contain a four-point control point spline");
12537 assert_eq!(
12538 updated_spline.controls.len(),
12539 4,
12540 "Expected edited spline to expose four control points"
12541 );
12542
12543 ctx.close().await;
12544 mock_ctx.close().await;
12545 }
12546
12547 #[tokio::test(flavor = "multi_thread")]
12548 async fn test_line_vertical() {
12549 let initial_source = "\
12550sketch(on = XY) {
12551 line(start = [var 1, var 2], end = [var 3, var 4])
12552}
12553";
12554
12555 let program = Program::parse(initial_source).unwrap().0.unwrap();
12556
12557 let mut frontend = FrontendState::new();
12558
12559 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12560 let mock_ctx = ExecutorContext::new_mock(None).await;
12561 let version = Version(0);
12562
12563 frontend.hack_set_program(&ctx, program).await.unwrap();
12564 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12565 let sketch_id = sketch_object.id;
12566 let sketch = expect_sketch(sketch_object);
12567 let line1_id = *sketch.segments.get(2).unwrap();
12568
12569 let constraint = Constraint::Vertical(Vertical::Line { line: line1_id });
12570 let (src_delta, scene_delta) = frontend
12571 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12572 .await
12573 .unwrap();
12574 insta::assert_snapshot!("test_line_vertical", src_delta.text.as_str());
12575 assert_eq!(
12576 scene_delta.new_graph.objects.len(),
12577 6,
12578 "{:#?}",
12579 scene_delta.new_graph.objects
12580 );
12581
12582 ctx.close().await;
12583 mock_ctx.close().await;
12584 }
12585
12586 #[tokio::test(flavor = "multi_thread")]
12587 async fn test_points_vertical() {
12588 let initial_source = "\
12589sketch001 = sketch(on = XY) {
12590 p0 = point(at = [var -2.23mm, var 3.1mm])
12591 pf = point(at = [4, 4])
12592}
12593";
12594
12595 let program = Program::parse(initial_source).unwrap().0.unwrap();
12596
12597 let mut frontend = FrontendState::new();
12598
12599 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12600 let mock_ctx = ExecutorContext::new_mock(None).await;
12601 let version = Version(0);
12602
12603 frontend.hack_set_program(&ctx, program).await.unwrap();
12604 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12605 let sketch_id = sketch_object.id;
12606 let sketch = expect_sketch(sketch_object);
12607 let point_ids = vec![
12608 sketch.segments.first().unwrap().to_owned(),
12609 sketch.segments.get(1).unwrap().to_owned(),
12610 ];
12611
12612 let constraint = Constraint::Vertical(Vertical::Points {
12613 points: point_ids.into_iter().map(ConstraintSegment::from).collect(),
12614 });
12615 let (src_delta, scene_delta) = frontend
12616 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12617 .await
12618 .unwrap();
12619 insta::assert_snapshot!("test_points_vertical", src_delta.text.as_str());
12620 assert_eq!(
12621 scene_delta.new_graph.objects.len(),
12622 5,
12623 "{:#?}",
12624 scene_delta.new_graph.objects
12625 );
12626
12627 ctx.close().await;
12628 mock_ctx.close().await;
12629 }
12630
12631 #[tokio::test(flavor = "multi_thread")]
12632 async fn test_points_horizontal() {
12633 let initial_source = "\
12634sketch001 = sketch(on = XY) {
12635 p0 = point(at = [var -2.23mm, var 3.1mm])
12636 pf = point(at = [4, 4])
12637}
12638";
12639
12640 let program = Program::parse(initial_source).unwrap().0.unwrap();
12641
12642 let mut frontend = FrontendState::new();
12643
12644 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12645 let mock_ctx = ExecutorContext::new_mock(None).await;
12646 let version = Version(0);
12647
12648 frontend.hack_set_program(&ctx, program).await.unwrap();
12649 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12650 let sketch_id = sketch_object.id;
12651 let sketch = expect_sketch(sketch_object);
12652 let point_ids = vec![
12653 sketch.segments.first().unwrap().to_owned(),
12654 sketch.segments.get(1).unwrap().to_owned(),
12655 ];
12656
12657 let constraint = Constraint::Horizontal(Horizontal::Points {
12658 points: point_ids.into_iter().map(ConstraintSegment::from).collect(),
12659 });
12660 let (src_delta, scene_delta) = frontend
12661 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12662 .await
12663 .unwrap();
12664 insta::assert_snapshot!("test_points_horizontal", src_delta.text.as_str());
12665 assert_eq!(
12666 scene_delta.new_graph.objects.len(),
12667 5,
12668 "{:#?}",
12669 scene_delta.new_graph.objects
12670 );
12671
12672 ctx.close().await;
12673 mock_ctx.close().await;
12674 }
12675
12676 #[tokio::test(flavor = "multi_thread")]
12677 async fn test_point_horizontal_with_origin() {
12678 let initial_source = "\
12679sketch001 = sketch(on = XY) {
12680 p0 = point(at = [var -2.23mm, var 3.1mm])
12681}
12682";
12683
12684 let program = Program::parse(initial_source).unwrap().0.unwrap();
12685
12686 let mut frontend = FrontendState::new();
12687
12688 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12689 let mock_ctx = ExecutorContext::new_mock(None).await;
12690 let version = Version(0);
12691
12692 frontend.hack_set_program(&ctx, program).await.unwrap();
12693 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12694 let sketch_id = sketch_object.id;
12695 let sketch = expect_sketch(sketch_object);
12696 let point_id = *sketch.segments.first().unwrap();
12697
12698 let constraint = Constraint::Horizontal(Horizontal::Points {
12699 points: vec![ConstraintSegment::from(point_id), ConstraintSegment::ORIGIN],
12700 });
12701 let (src_delta, scene_delta) = frontend
12702 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12703 .await
12704 .unwrap();
12705 insta::assert_snapshot!("test_point_horizontal_with_origin", src_delta.text.as_str());
12706 assert_eq!(
12707 scene_delta.new_graph.objects.len(),
12708 4,
12709 "{:#?}",
12710 scene_delta.new_graph.objects
12711 );
12712
12713 ctx.close().await;
12714 mock_ctx.close().await;
12715 }
12716
12717 #[tokio::test(flavor = "multi_thread")]
12718 async fn test_lines_equal_length() {
12719 let initial_source = "\
12720sketch(on = XY) {
12721 line(start = [var 1, var 2], end = [var 3, var 4])
12722 line(start = [var 5, var 6], end = [var 7, var 8])
12723}
12724";
12725
12726 let program = Program::parse(initial_source).unwrap().0.unwrap();
12727
12728 let mut frontend = FrontendState::new();
12729
12730 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12731 let mock_ctx = ExecutorContext::new_mock(None).await;
12732 let version = Version(0);
12733
12734 frontend.hack_set_program(&ctx, program).await.unwrap();
12735 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12736 let sketch_id = sketch_object.id;
12737 let sketch = expect_sketch(sketch_object);
12738 let line1_id = *sketch.segments.get(2).unwrap();
12739 let line2_id = *sketch.segments.get(5).unwrap();
12740
12741 let constraint = Constraint::LinesEqualLength(LinesEqualLength {
12742 lines: vec![line1_id, line2_id],
12743 });
12744 let (src_delta, scene_delta) = frontend
12745 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12746 .await
12747 .unwrap();
12748 insta::assert_snapshot!("test_lines_equal_length", src_delta.text.as_str());
12749 assert_eq!(
12750 scene_delta.new_graph.objects.len(),
12751 9,
12752 "{:#?}",
12753 scene_delta.new_graph.objects
12754 );
12755
12756 ctx.close().await;
12757 mock_ctx.close().await;
12758 }
12759
12760 #[tokio::test(flavor = "multi_thread")]
12761 async fn test_add_constraint_multi_line_equal_length() {
12762 let initial_source = "\
12763sketch(on = XY) {
12764 line(start = [var 1, var 2], end = [var 3, var 4])
12765 line(start = [var 5, var 6], end = [var 7, var 8])
12766 line(start = [var 9, var 10], end = [var 11, var 12])
12767}
12768";
12769
12770 let program = Program::parse(initial_source).unwrap().0.unwrap();
12771
12772 let mut frontend = FrontendState::new();
12773 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12774 let mock_ctx = ExecutorContext::new_mock(None).await;
12775 let version = Version(0);
12776
12777 frontend.hack_set_program(&ctx, program).await.unwrap();
12778 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12779 let sketch_id = sketch_object.id;
12780 let sketch = expect_sketch(sketch_object);
12781 let line1_id = *sketch.segments.get(2).unwrap();
12782 let line2_id = *sketch.segments.get(5).unwrap();
12783 let line3_id = *sketch.segments.get(8).unwrap();
12784
12785 let constraint = Constraint::LinesEqualLength(LinesEqualLength {
12786 lines: vec![line1_id, line2_id, line3_id],
12787 });
12788 let (src_delta, scene_delta) = frontend
12789 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12790 .await
12791 .unwrap();
12792 insta::assert_snapshot!("test_add_constraint_multi_line_equal_length", src_delta.text.as_str());
12793 let constraints = scene_delta
12794 .new_graph
12795 .objects
12796 .iter()
12797 .filter_map(|obj| {
12798 let ObjectKind::Constraint { constraint } = &obj.kind else {
12799 return None;
12800 };
12801 Some(constraint)
12802 })
12803 .collect::<Vec<_>>();
12804
12805 assert_eq!(constraints.len(), 1, "{:#?}", frontend.scene_graph.objects);
12806 let Constraint::LinesEqualLength(lines_equal_length) = constraints[0] else {
12807 panic!("expected equal length constraint, got {:?}", constraints[0]);
12808 };
12809 assert_eq!(lines_equal_length.lines.len(), 3);
12810
12811 ctx.close().await;
12812 mock_ctx.close().await;
12813 }
12814
12815 #[tokio::test(flavor = "multi_thread")]
12816 async fn test_lines_parallel() {
12817 let initial_source = "\
12818sketch(on = XY) {
12819 line(start = [var 1, var 2], end = [var 3, var 4])
12820 line(start = [var 5, var 6], end = [var 7, var 8])
12821}
12822";
12823
12824 let program = Program::parse(initial_source).unwrap().0.unwrap();
12825
12826 let mut frontend = FrontendState::new();
12827
12828 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12829 let mock_ctx = ExecutorContext::new_mock(None).await;
12830 let version = Version(0);
12831
12832 frontend.hack_set_program(&ctx, program).await.unwrap();
12833 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12834 let sketch_id = sketch_object.id;
12835 let sketch = expect_sketch(sketch_object);
12836 let line1_id = *sketch.segments.get(2).unwrap();
12837 let line2_id = *sketch.segments.get(5).unwrap();
12838
12839 let constraint = Constraint::Parallel(Parallel {
12840 lines: vec![line1_id, line2_id],
12841 });
12842 let (src_delta, scene_delta) = frontend
12843 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12844 .await
12845 .unwrap();
12846 insta::assert_snapshot!("test_lines_parallel", src_delta.text.as_str());
12847 assert_eq!(
12848 scene_delta.new_graph.objects.len(),
12849 9,
12850 "{:#?}",
12851 scene_delta.new_graph.objects
12852 );
12853
12854 ctx.close().await;
12855 mock_ctx.close().await;
12856 }
12857
12858 #[tokio::test(flavor = "multi_thread")]
12859 async fn test_lines_parallel_multiline() {
12860 let initial_source = "\
12861sketch(on = XY) {
12862 line(start = [var 1, var 2], end = [var 3, var 4])
12863 line(start = [var 5, var 6], end = [var 7, var 8])
12864 line(start = [var 9, var 10], end = [var 11, var 12])
12865}
12866";
12867
12868 let program = Program::parse(initial_source).unwrap().0.unwrap();
12869
12870 let mut frontend = FrontendState::new();
12871
12872 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12873 let mock_ctx = ExecutorContext::new_mock(None).await;
12874 let version = Version(0);
12875
12876 frontend.hack_set_program(&ctx, program).await.unwrap();
12877 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12878 let sketch_id = sketch_object.id;
12879 let sketch = expect_sketch(sketch_object);
12880 let line1_id = *sketch.segments.get(2).unwrap();
12881 let line2_id = *sketch.segments.get(5).unwrap();
12882 let line3_id = *sketch.segments.get(8).unwrap();
12883
12884 let constraint = Constraint::Parallel(Parallel {
12885 lines: vec![line1_id, line2_id, line3_id],
12886 });
12887 let (src_delta, scene_delta) = frontend
12888 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12889 .await
12890 .unwrap();
12891 insta::assert_snapshot!("test_lines_parallel_multiline", src_delta.text.as_str());
12892
12893 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
12894 let sketch = expect_sketch(sketch_object);
12895 assert_eq!(sketch.constraints.len(), 1);
12896
12897 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
12898 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12899 panic!("Expected constraint object");
12900 };
12901 let Constraint::Parallel(parallel) = constraint else {
12902 panic!("Expected parallel constraint");
12903 };
12904 assert_eq!(parallel.lines.len(), 3);
12905
12906 ctx.close().await;
12907 mock_ctx.close().await;
12908 }
12909
12910 #[tokio::test(flavor = "multi_thread")]
12911 async fn test_lines_perpendicular() {
12912 let initial_source = "\
12913sketch(on = XY) {
12914 line(start = [var 1, var 2], end = [var 3, var 4])
12915 line(start = [var 5, var 6], end = [var 7, var 8])
12916}
12917";
12918
12919 let program = Program::parse(initial_source).unwrap().0.unwrap();
12920
12921 let mut frontend = FrontendState::new();
12922
12923 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12924 let mock_ctx = ExecutorContext::new_mock(None).await;
12925 let version = Version(0);
12926
12927 frontend.hack_set_program(&ctx, program).await.unwrap();
12928 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12929 let sketch_id = sketch_object.id;
12930 let sketch = expect_sketch(sketch_object);
12931 let line1_id = *sketch.segments.get(2).unwrap();
12932 let line2_id = *sketch.segments.get(5).unwrap();
12933
12934 let constraint = Constraint::Perpendicular(Perpendicular {
12935 lines: vec![line1_id, line2_id],
12936 });
12937 let (src_delta, scene_delta) = frontend
12938 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12939 .await
12940 .unwrap();
12941 insta::assert_snapshot!("test_lines_perpendicular", src_delta.text.as_str());
12942 assert_eq!(
12943 scene_delta.new_graph.objects.len(),
12944 9,
12945 "{:#?}",
12946 scene_delta.new_graph.objects
12947 );
12948
12949 ctx.close().await;
12950 mock_ctx.close().await;
12951 }
12952
12953 #[tokio::test(flavor = "multi_thread")]
12954 async fn test_lines_angle() {
12955 let initial_source = "\
12956sketch(on = XY) {
12957 line(start = [var 1, var 2], end = [var 3, var 4])
12958 line(start = [var 5, var 6], end = [var 7, var 8])
12959}
12960";
12961
12962 let program = Program::parse(initial_source).unwrap().0.unwrap();
12963
12964 let mut frontend = FrontendState::new();
12965
12966 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12967 let mock_ctx = ExecutorContext::new_mock(None).await;
12968 let version = Version(0);
12969
12970 frontend.hack_set_program(&ctx, program).await.unwrap();
12971 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12972 let sketch_id = sketch_object.id;
12973 let sketch = expect_sketch(sketch_object);
12974 let line1_id = *sketch.segments.get(2).unwrap();
12975 let line2_id = *sketch.segments.get(5).unwrap();
12976
12977 let constraint = Constraint::Angle(Angle {
12978 lines: vec![line1_id, line2_id],
12979 angle: Number {
12980 value: 30.0,
12981 units: NumericSuffix::Deg,
12982 },
12983 source: Default::default(),
12984 });
12985 let (src_delta, scene_delta) = frontend
12986 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12987 .await
12988 .unwrap();
12989 insta::assert_snapshot!("test_lines_angle", src_delta.text.as_str());
12990 assert_eq!(
12991 scene_delta.new_graph.objects.len(),
12992 9,
12993 "{:#?}",
12994 scene_delta.new_graph.objects
12995 );
12996
12997 ctx.close().await;
12998 mock_ctx.close().await;
12999 }
13000
13001 #[tokio::test(flavor = "multi_thread")]
13002 async fn test_segments_tangent() {
13003 let initial_source = "\
13004sketch(on = XY) {
13005 line(start = [var 1, var 2], end = [var 3, var 4])
13006 arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
13007}
13008";
13009
13010 let program = Program::parse(initial_source).unwrap().0.unwrap();
13011
13012 let mut frontend = FrontendState::new();
13013
13014 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13015 let mock_ctx = ExecutorContext::new_mock(None).await;
13016 let version = Version(0);
13017
13018 frontend.hack_set_program(&ctx, program).await.unwrap();
13019 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13020 let sketch_id = sketch_object.id;
13021 let sketch = expect_sketch(sketch_object);
13022 let line1_id = *sketch.segments.get(2).unwrap();
13023 let arc1_id = *sketch.segments.get(6).unwrap();
13024
13025 let constraint = Constraint::Tangent(Tangent {
13026 input: vec![line1_id, arc1_id],
13027 });
13028 let (src_delta, scene_delta) = frontend
13029 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13030 .await
13031 .unwrap();
13032 insta::assert_snapshot!("test_segments_tangent", src_delta.text.as_str());
13033 assert_eq!(
13034 scene_delta.new_graph.objects.len(),
13035 10,
13036 "{:#?}",
13037 scene_delta.new_graph.objects
13038 );
13039
13040 ctx.close().await;
13041 mock_ctx.close().await;
13042 }
13043
13044 #[tokio::test(flavor = "multi_thread")]
13045 async fn test_point_midpoint() {
13046 let initial_source = "\
13047sketch(on = XY) {
13048 point(at = [var 1, var 1])
13049 line(start = [var 0, var 0], end = [var 6, var 4])
13050}
13051";
13052
13053 let program = Program::parse(initial_source).unwrap().0.unwrap();
13054
13055 let mut frontend = FrontendState::new();
13056
13057 let ctx = ExecutorContext::new_mock(None).await;
13058 let version = Version(0);
13059
13060 frontend.program = program.clone();
13061 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
13062 frontend.update_state_after_exec(outcome, true);
13063 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13064 let sketch_id = sketch_object.id;
13065 let sketch = expect_sketch(sketch_object);
13066 let point_id = *sketch.segments.first().unwrap();
13067 let line_id = *sketch.segments.get(3).unwrap();
13068
13069 let constraint = Constraint::Midpoint(Midpoint {
13070 point: ConstraintSegment::from(point_id),
13071 segment: line_id,
13072 });
13073 let (src_delta, scene_delta) = frontend
13074 .add_constraint(&ctx, version, sketch_id, constraint)
13075 .await
13076 .unwrap();
13077 insta::assert_snapshot!("test_point_midpoint", src_delta.text.as_str());
13078 assert_eq!(
13079 scene_delta.new_graph.objects.len(),
13080 7,
13081 "{:#?}",
13082 scene_delta.new_graph.objects
13083 );
13084
13085 ctx.close().await;
13086 }
13087
13088 #[tokio::test(flavor = "multi_thread")]
13089 async fn test_segments_symmetric() {
13090 let initial_source = "\
13091sketch(on = XY) {
13092 line(start = [var 0, var 0], end = [var 0, var 4])
13093 line(start = [var 4, var 0], end = [var 4, var 4])
13094 line(start = [var 2, var -1], end = [var 2, var 5])
13095}
13096";
13097
13098 let program = Program::parse(initial_source).unwrap().0.unwrap();
13099
13100 let mut frontend = FrontendState::new();
13101
13102 let ctx = ExecutorContext::new_mock(None).await;
13103 let version = Version(0);
13104
13105 frontend.program = program.clone();
13106 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
13107 frontend.update_state_after_exec(outcome, true);
13108 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13109 let sketch_id = sketch_object.id;
13110 let sketch = expect_sketch(sketch_object);
13111 let line1_id = *sketch.segments.get(2).unwrap();
13112 let line2_id = *sketch.segments.get(5).unwrap();
13113 let axis_id = *sketch.segments.get(8).unwrap();
13114
13115 let constraint = Constraint::Symmetric(Symmetric {
13116 input: vec![line1_id, line2_id],
13117 axis: axis_id,
13118 });
13119 let (src_delta, scene_delta) = frontend
13120 .add_constraint(&ctx, version, sketch_id, constraint)
13121 .await
13122 .unwrap();
13123 insta::assert_snapshot!("test_segments_symmetric", src_delta.text.as_str());
13124 assert_eq!(
13125 scene_delta.new_graph.objects.len(),
13126 12,
13127 "{:#?}",
13128 scene_delta.new_graph.objects
13129 );
13130
13131 ctx.close().await;
13132 }
13133
13134 #[tokio::test(flavor = "multi_thread")]
13135 async fn test_point_arc_midpoint() {
13136 let initial_source = "\
13137sketch(on = XY) {
13138 point(at = [var 6, var 3])
13139 arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
13140}
13141";
13142
13143 let program = Program::parse(initial_source).unwrap().0.unwrap();
13144
13145 let mut frontend = FrontendState::new();
13146
13147 let ctx = ExecutorContext::new_mock(None).await;
13148 let version = Version(0);
13149
13150 frontend.program = program.clone();
13151 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
13152 frontend.update_state_after_exec(outcome, true);
13153 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13154 let sketch_id = sketch_object.id;
13155 let sketch = expect_sketch(sketch_object);
13156 let point_id = *sketch.segments.first().unwrap();
13157 let arc_id = *sketch.segments.get(4).unwrap();
13158
13159 let constraint = Constraint::Midpoint(Midpoint {
13160 point: ConstraintSegment::from(point_id),
13161 segment: arc_id,
13162 });
13163 let (src_delta, scene_delta) = frontend
13164 .add_constraint(&ctx, version, sketch_id, constraint)
13165 .await
13166 .unwrap();
13167 insta::assert_snapshot!("test_point_arc_midpoint", src_delta.text.as_str());
13168 assert_eq!(
13169 scene_delta.new_graph.objects.len(),
13170 8,
13171 "{:#?}",
13172 scene_delta.new_graph.objects
13173 );
13174
13175 ctx.close().await;
13176 }
13177
13178 #[tokio::test(flavor = "multi_thread")]
13179 async fn test_origin_line_midpoint() {
13180 let initial_source = "\
13181sketch(on = XY) {
13182 line(start = [var 0, var 0], end = [var 6, var 4])
13183}
13184";
13185
13186 let program = Program::parse(initial_source).unwrap().0.unwrap();
13187
13188 let mut frontend = FrontendState::new();
13189
13190 let ctx = ExecutorContext::new_mock(None).await;
13191 let version = Version(0);
13192
13193 frontend.program = program.clone();
13194 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
13195 frontend.update_state_after_exec(outcome, true);
13196 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13197 let sketch_id = sketch_object.id;
13198 let sketch = expect_sketch(sketch_object);
13199 let line_id = *sketch.segments.get(2).unwrap();
13200
13201 let constraint = Constraint::Midpoint(Midpoint {
13202 point: ConstraintSegment::ORIGIN,
13203 segment: line_id,
13204 });
13205 let (src_delta, scene_delta) = frontend
13206 .add_constraint(&ctx, version, sketch_id, constraint)
13207 .await
13208 .unwrap();
13209 insta::assert_snapshot!("test_origin_line_midpoint", src_delta.text.as_str());
13210 assert_eq!(
13211 scene_delta.new_graph.objects.len(),
13212 6,
13213 "{:#?}",
13214 scene_delta.new_graph.objects
13215 );
13216
13217 ctx.close().await;
13218 }
13219
13220 #[tokio::test(flavor = "multi_thread")]
13221 async fn test_origin_arc_midpoint() {
13222 let initial_source = "\
13223sketch(on = XY) {
13224 arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
13225}
13226";
13227
13228 let program = Program::parse(initial_source).unwrap().0.unwrap();
13229
13230 let mut frontend = FrontendState::new();
13231
13232 let ctx = ExecutorContext::new_mock(None).await;
13233 let version = Version(0);
13234
13235 frontend.program = program.clone();
13236 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
13237 frontend.update_state_after_exec(outcome, true);
13238 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13239 let sketch_id = sketch_object.id;
13240 let sketch = expect_sketch(sketch_object);
13241 let arc_id = *sketch.segments.get(3).unwrap();
13242
13243 let constraint = Constraint::Midpoint(Midpoint {
13244 point: ConstraintSegment::ORIGIN,
13245 segment: arc_id,
13246 });
13247 let (src_delta, scene_delta) = frontend
13248 .add_constraint(&ctx, version, sketch_id, constraint)
13249 .await
13250 .unwrap();
13251 insta::assert_snapshot!("test_origin_arc_midpoint", src_delta.text.as_str());
13252 assert_eq!(
13253 scene_delta.new_graph.objects.len(),
13254 7,
13255 "{:#?}",
13256 scene_delta.new_graph.objects
13257 );
13258
13259 ctx.close().await;
13260 }
13261
13262 #[tokio::test(flavor = "multi_thread")]
13263 async fn test_segments_symmetric_arcs() {
13264 let initial_source = "\
13265sketch(on = XY) {
13266 arc(start = [var -15, var 0], end = [var -10, var 5], center = [var -10, var 0])
13267 arc(start = [var 6, var 2], end = [var 12, var -4], center = [var 8, var 1])
13268 line(start = [var 0, var -10], end = [var 0, var 10])
13269}
13270";
13271
13272 let program = Program::parse(initial_source).unwrap().0.unwrap();
13273
13274 let mut frontend = FrontendState::new();
13275
13276 let ctx = ExecutorContext::new_mock(None).await;
13277 let version = Version(0);
13278
13279 frontend.program = program.clone();
13280 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
13281 frontend.update_state_after_exec(outcome, true);
13282 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13283 let sketch_id = sketch_object.id;
13284 let sketch = expect_sketch(sketch_object);
13285 let arc1_id = *sketch.segments.get(3).unwrap();
13286 let arc2_id = *sketch.segments.get(7).unwrap();
13287 let axis_id = *sketch.segments.get(10).unwrap();
13288
13289 let constraint = Constraint::Symmetric(Symmetric {
13290 input: vec![arc1_id, arc2_id],
13291 axis: axis_id,
13292 });
13293 let (src_delta, scene_delta) = frontend
13294 .add_constraint(&ctx, version, sketch_id, constraint)
13295 .await
13296 .unwrap();
13297 insta::assert_snapshot!("test_segments_symmetric_arcs", src_delta.text.as_str());
13298 assert_eq!(
13299 scene_delta.new_graph.objects.len(),
13300 14,
13301 "{:#?}",
13302 scene_delta.new_graph.objects
13303 );
13304
13305 ctx.close().await;
13306 }
13307
13308 #[tokio::test(flavor = "multi_thread")]
13309 async fn test_sketch_on_face_simple() {
13310 let initial_source = "\
13311len = 2mm
13312cube = startSketchOn(XY)
13313 |> startProfile(at = [0, 0])
13314 |> line(end = [len, 0], tag = $side)
13315 |> line(end = [0, len])
13316 |> line(end = [-len, 0])
13317 |> line(end = [0, -len])
13318 |> close()
13319 |> extrude(length = len)
13320
13321face = faceOf(cube, face = side)
13322";
13323
13324 let program = Program::parse(initial_source).unwrap().0.unwrap();
13325
13326 let mut frontend = FrontendState::new();
13327
13328 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13329 let mock_ctx = ExecutorContext::new_mock(None).await;
13330 let version = Version(0);
13331
13332 frontend.hack_set_program(&ctx, program).await.unwrap();
13333 let face_object = find_first_face_object(&frontend.scene_graph).unwrap();
13334 let face_id = face_object.id;
13335
13336 let sketch_args = SketchCtor {
13337 on: Plane::Object(face_id),
13338 };
13339 let (_src_delta, scene_delta, sketch_id) = frontend
13340 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
13341 .await
13342 .unwrap();
13343 assert_eq!(sketch_id, ObjectId(2));
13344 assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
13345 let sketch_object = &scene_delta.new_graph.objects[2];
13346 assert_eq!(sketch_object.id, ObjectId(2));
13347 assert_eq!(
13348 sketch_object.kind,
13349 ObjectKind::Sketch(Sketch {
13350 args: SketchCtor {
13351 on: Plane::Object(face_id),
13352 },
13353 plane: face_id,
13354 segments: vec![],
13355 constraints: vec![],
13356 })
13357 );
13358 assert_eq!(scene_delta.new_graph.objects.len(), 8);
13359
13360 ctx.close().await;
13361 mock_ctx.close().await;
13362 }
13363
13364 #[tokio::test(flavor = "multi_thread")]
13365 async fn test_sketch_on_wall_artifact_from_region_extrude() {
13366 let initial_source = "\
13367s = sketch(on = YZ) {
13368 line1 = line(start = [0, 0], end = [0, 1])
13369 line2 = line(start = [0, 1], end = [1, 1])
13370 line3 = line(start = [1, 1], end = [0, 0])
13371}
13372region001 = region(point = [0.1, 0.1], sketch = s)
13373extrude001 = extrude(region001, length = 5)
13374";
13375
13376 let program = Program::parse(initial_source).unwrap().0.unwrap();
13377
13378 let mut frontend = FrontendState::new();
13379 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13380 let version = Version(0);
13381
13382 frontend.hack_set_program(&ctx, program).await.unwrap();
13383 let wall_object_id = find_first_wall_object_id(&frontend.scene_graph).expect("expected a wall object");
13384
13385 let sketch_args = SketchCtor {
13386 on: Plane::Object(wall_object_id),
13387 };
13388 let (src_delta, _scene_delta, _sketch_id) = frontend
13389 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
13390 .await
13391 .unwrap();
13392 assert!(src_delta.text.contains("faceOf(extrude001, face = region001.tags."));
13393
13394 ctx.close().await;
13395 }
13396
13397 #[tokio::test(flavor = "multi_thread")]
13398 async fn test_sketch_on_wall_artifact_from_split_region_extrude() {
13399 let initial_source = "\
13400sketch001 = sketch(on = YZ) {
13401 line1 = line(start = [var 0.49, var -0.39], end = [var 6.52, var -0.39])
13402 line2 = line(start = [var 6.52, var -0.39], end = [var 6.52, var 4.9])
13403 line3 = line(start = [var 6.52, var 4.9], end = [var 0.49, var 4.9])
13404 line4 = line(start = [var 0.49, var 4.9], end = [var 0.49, var -0.39])
13405 coincident([line1.end, line2.start])
13406 coincident([line2.end, line3.start])
13407 coincident([line3.end, line4.start])
13408 coincident([line4.end, line1.start])
13409 parallel([line2, line4])
13410 parallel([line3, line1])
13411 perpendicular([line1, line2])
13412 horizontal(line3)
13413 line5 = line(start = [2.35, 6.65], end = [5.89, -2.7])
13414}
13415region001 = region(point = [3.1, 3.74], sketch = sketch001)
13416extrude001 = extrude(region001, length = 5)
13417";
13418
13419 let program = Program::parse(initial_source).unwrap().0.unwrap();
13420
13421 let mut frontend = FrontendState::new();
13422 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13423 let version = Version(0);
13424
13425 frontend.hack_set_program(&ctx, program).await.unwrap();
13426 let wall_object_id = find_first_wall_object_id(&frontend.scene_graph).expect("expected a wall object");
13427
13428 let sketch_args = SketchCtor {
13429 on: Plane::Object(wall_object_id),
13430 };
13431 let (src_delta, _scene_delta, _sketch_id) = frontend
13432 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
13433 .await
13434 .unwrap();
13435 assert!(src_delta.text.contains("faceOf(extrude001, face = region001.tags."));
13436
13437 ctx.close().await;
13438 }
13439
13440 #[tokio::test(flavor = "multi_thread")]
13441 async fn test_new_sketch_on_multi_region_extrude_cap_indexes_selected_solid() {
13442 let initial_source = "\
13443@settings(kclVersion = 2.0)
13444
13445sketch001 = sketch(on = XY) {
13446 circle1 = circle(start = [var -1.51mm, var 1.31mm], center = [var -1.98mm, var 1.03mm])
13447 circle2 = circle(start = [var 2.98mm, var 2.37mm], center = [var 2.66mm, var 1.46mm])
13448}
13449hidden001 = hide(sketch001)
13450region001 = region(segments = [sketch001.circle2])
13451region002 = region(segments = [sketch001.circle1])
13452extrude001 = extrude([region001, region002], length = 5)
13453";
13454
13455 let program = Program::parse(initial_source).unwrap().0.unwrap();
13456 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13457 let version = Version(0);
13458
13459 for (solid_output_index, expected_face) in [
13460 (0, "faceOf(extrude001[0], face = END)"),
13461 (1, "faceOf(extrude001[1], face = END)"),
13462 ] {
13463 let mut frontend = FrontendState::new();
13464 frontend.hack_set_program(&ctx, program.clone()).await.unwrap();
13465 let cap_object_id = find_cap_object_id_with_solid_output_index(
13466 &frontend.scene_graph,
13467 crate::frontend::api::CapKind::End,
13468 solid_output_index,
13469 )
13470 .unwrap_or_else(|| panic!("expected an end cap object for solid output index {solid_output_index}"));
13471
13472 let sketch_args = SketchCtor {
13473 on: Plane::Object(cap_object_id),
13474 };
13475 let (src_delta, _scene_delta, _sketch_id) = frontend
13476 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
13477 .await
13478 .unwrap();
13479
13480 assert!(
13481 src_delta.text.contains(expected_face),
13482 "expected `{expected_face}` in:\n{}",
13483 src_delta.text
13484 );
13485 assert!(!src_delta.text.contains("faceOf(extrude001, face = END)"));
13486 }
13487
13488 ctx.close().await;
13489 }
13490
13491 #[tokio::test(flavor = "multi_thread")]
13492 async fn test_new_sketch_on_multi_region_extrude_wall_indexes_selected_solid() {
13493 let initial_source = "\
13494@settings(kclVersion = 2.0)
13495
13496sketch001 = sketch(on = XY) {
13497 rect1Line1 = line(start = [0, 0], end = [1, 0])
13498 rect1Line2 = line(start = [1, 0], end = [1, 1])
13499 rect1Line3 = line(start = [1, 1], end = [0, 1])
13500 rect1Line4 = line(start = [0, 1], end = [0, 0])
13501 rect2Line1 = line(start = [3, 0], end = [4, 0])
13502 rect2Line2 = line(start = [4, 0], end = [4, 1])
13503 rect2Line3 = line(start = [4, 1], end = [3, 1])
13504 rect2Line4 = line(start = [3, 1], end = [3, 0])
13505}
13506hidden001 = hide(sketch001)
13507region001 = region(segments = [
13508 sketch001.rect1Line4,
13509 sketch001.rect1Line1
13510])
13511region002 = region(segments = [
13512 sketch001.rect2Line4,
13513 sketch001.rect2Line1
13514])
13515extrude001 = extrude([region001, region002], length = 5)
13516";
13517
13518 let program = Program::parse(initial_source).unwrap().0.unwrap();
13519 let mut frontend = FrontendState::new();
13520 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13521 let version = Version(0);
13522
13523 frontend.hack_set_program(&ctx, program).await.unwrap();
13524 let region_call = "\
13525region(segments = [
13526 sketch001.rect1Line4,
13527 sketch001.rect1Line1
13528])";
13529 let region_call_start = initial_source.find(region_call).unwrap();
13530 let region_range = [region_call_start, region_call_start + region_call.len(), 0].into();
13531 let segment_call = "line(start = [0, 0], end = [1, 0])";
13532 let segment_call_start = initial_source.find(segment_call).unwrap();
13533 let segment_range = [segment_call_start, segment_call_start + segment_call.len(), 0].into();
13534 let wall_object_id = frontend
13535 .scene_graph
13536 .objects
13537 .iter()
13538 .find_map(|object| match &object.kind {
13539 ObjectKind::Wall(wall)
13540 if wall.source.path.as_ref().is_some_and(|path| path.range == region_range)
13541 && wall.source.segment.range == segment_range =>
13542 {
13543 Some(object.id)
13544 }
13545 _ => None,
13546 })
13547 .expect("expected a wall object for region001.tags.rect1Line1");
13548
13549 let sketch_args = SketchCtor {
13550 on: Plane::Object(wall_object_id),
13551 };
13552 let (src_delta, _scene_delta, _sketch_id) = frontend
13553 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
13554 .await
13555 .unwrap();
13556
13557 let expected_face = "faceOf(extrude001[0], face = region001.tags.rect1Line1)";
13558 assert!(
13559 src_delta.text.contains(expected_face),
13560 "expected `{expected_face}` in:\n{}",
13561 src_delta.text
13562 );
13563 assert!(!src_delta.text.contains("faceOf(extrude001, face ="));
13564
13565 ctx.close().await;
13566 }
13567
13568 #[test]
13569 fn test_enclosing_variable_fallback_skips_nested_sketch_items() {
13570 let source = "\
13571sketch001 = sketch(on = XY) {
13572 line(start = [0, 0], end = [1, 0])
13573}
13574part = subtract(boxSolid, tools = [cutSolid])
13575 |> appearance(color = \"#8f96a3\")
13576";
13577 let ast = Program::parse(source).unwrap().0.unwrap().ast;
13578 let line_start = source.find("line").unwrap();
13579 let line_end = line_start + "line(start = [0, 0], end = [1, 0])".len();
13580 let line_ref = SourceRef::Simple {
13581 range: [line_start, line_end, 0].into(),
13582 node_path: None,
13583 };
13584 assert_eq!(variable_name_containing_source_ref(&ast, &line_ref), None);
13585
13586 let subtract_start = source.find("subtract").unwrap();
13587 let subtract_end = subtract_start + "subtract(boxSolid, tools = [cutSolid])".len();
13588 let subtract_ref = SourceRef::Simple {
13589 range: [subtract_start, subtract_end, 0].into(),
13590 node_path: None,
13591 };
13592 assert_eq!(
13593 variable_name_containing_source_ref(&ast, &subtract_ref),
13594 Some("part".to_owned())
13595 );
13596 }
13597
13598 #[tokio::test(flavor = "multi_thread")]
13599 async fn test_sketch_on_subtracted_sweep_cap_uses_composite_solid() {
13600 clear_mem_cache().await;
13601 let source = "\
13602boxSolid = startSketchOn(XY)
13603 |> startProfile(at = [0, 0])
13604 |> line(end = [4, 0], tag = $bottomEdge)
13605 |> line(end = [0, 4])
13606 |> line(end = [-4, 0])
13607 |> close()
13608 |> extrude(length = 10)
13609cutSolid = startSketchOn(XY)
13610 |> startProfile(at = [1, 1])
13611 |> line(end = [1, 0])
13612 |> line(end = [0, 1])
13613 |> line(end = [-1, 0])
13614 |> close()
13615 |> extrude(length = 10)
13616part = subtract(boxSolid, tools = [cutSolid])
13617 |> appearance(color = \"#8f96a3\", roughness = 55, metalness = 8)
13618";
13619 let program = Program::parse(source).unwrap().0.unwrap();
13620 let mut frontend = FrontendState::new();
13621 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13622 match frontend.hack_set_program(&ctx, program).await.unwrap() {
13623 SetProgramOutcome::Success { .. } => {}
13624 SetProgramOutcome::ExecFailure { error } => panic!("KCL fixture failed to execute: {error:?}"),
13625 }
13626
13627 let sweep_call_start = source.find("extrude").unwrap();
13628 let sweep_call_end = sweep_call_start + "extrude(length = 10)".len();
13629 let part_call_start = source.find("subtract").unwrap();
13630 let part_call_end = part_call_start + "subtract(boxSolid, tools = [cutSolid])".len();
13631 let sweep_range = [sweep_call_start, sweep_call_end, 0].into();
13632 let composite_range = [part_call_start, part_call_end, 0].into();
13633
13634 let cap_object = frontend
13635 .scene_graph
13636 .objects
13637 .iter()
13638 .find(|object| {
13639 matches!(
13640 &object.kind,
13641 ObjectKind::Cap(crate::frontend::api::Cap {
13642 kind: crate::frontend::api::CapKind::End,
13643 source,
13644 ..
13645 }) if source.solid.range == composite_range && source.sweep.range == sweep_range
13646 )
13647 })
13648 .expect("expected end cap object to trace through subtract and original extrude");
13649
13650 let mut ast = frontend.program.ast.clone();
13651 let cap_expr = sketch_on_ast_expr(&mut ast, &frontend.scene_graph, &Plane::Object(cap_object.id)).unwrap();
13652 let cap_face_decl = ast::VariableDeclaration::new(
13653 ast::VariableDeclarator::new("capFace", cap_expr.clone()),
13654 ast::ItemVisibility::Default,
13655 ast::VariableKind::Const,
13656 );
13657 ast.body
13658 .push(ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(
13659 cap_face_decl,
13660 ))));
13661 let generated_source = source_from_ast(&ast);
13662
13663 assert!(generated_source.contains("capFace = faceOf(part, face = END)"));
13664 assert!(!generated_source.contains("faceOf(boxSolid"));
13665 let ast::Expr::CallExpressionKw(call) = cap_expr else {
13666 panic!("expected faceOf call");
13667 };
13668 assert_eq!(call.callee.name.name, "faceOf");
13669 let ast::Expr::Name(solid_name) = call.unlabeled.as_ref().unwrap() else {
13670 panic!("expected solid name");
13671 };
13672 assert_eq!(solid_name.name.name, "part");
13673 let ast::Expr::Name(face_name) = &call.arguments[0].arg else {
13674 panic!("expected face name");
13675 };
13676 assert_eq!(face_name.name.name, "END");
13677
13678 ctx.close().await;
13679 }
13680
13681 #[tokio::test(flavor = "multi_thread")]
13682 async fn test_sketch_on_plane_incremental() {
13683 let initial_source = "\
13684len = 2mm
13685cube = startSketchOn(XY)
13686 |> startProfile(at = [0, 0])
13687 |> line(end = [len, 0], tag = $side)
13688 |> line(end = [0, len])
13689 |> line(end = [-len, 0])
13690 |> line(end = [0, -len])
13691 |> close()
13692 |> extrude(length = len)
13693
13694plane = planeOf(cube, face = side)
13695";
13696
13697 let program = Program::parse(initial_source).unwrap().0.unwrap();
13698
13699 let mut frontend = FrontendState::new();
13700
13701 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13702 let mock_ctx = ExecutorContext::new_mock(None).await;
13703 let version = Version(0);
13704
13705 frontend.hack_set_program(&ctx, program).await.unwrap();
13706 let plane_object = frontend
13708 .scene_graph
13709 .objects
13710 .iter()
13711 .rev()
13712 .find(|object| matches!(&object.kind, ObjectKind::Plane(_)))
13713 .unwrap();
13714 let plane_id = plane_object.id;
13715
13716 let sketch_args = SketchCtor {
13717 on: Plane::Object(plane_id),
13718 };
13719 let (src_delta, scene_delta, sketch_id) = frontend
13720 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
13721 .await
13722 .unwrap();
13723 insta::assert_snapshot!("test_sketch_on_plane_incremental", src_delta.text.as_str());
13724 assert_eq!(sketch_id, ObjectId(2));
13725 assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
13726 let sketch_object = &scene_delta.new_graph.objects[2];
13727 assert_eq!(sketch_object.id, ObjectId(2));
13728 assert_eq!(
13729 sketch_object.kind,
13730 ObjectKind::Sketch(Sketch {
13731 args: SketchCtor {
13732 on: Plane::Object(plane_id),
13733 },
13734 plane: plane_id,
13735 segments: vec![],
13736 constraints: vec![],
13737 })
13738 );
13739 assert_eq!(scene_delta.new_graph.objects.len(), 9);
13740
13741 let plane_object = scene_delta.new_graph.objects.get(plane_id.0).unwrap();
13742 assert_eq!(plane_object.id, plane_id);
13743 assert_eq!(plane_object.kind, ObjectKind::Plane(Plane::Object(plane_id)));
13744
13745 ctx.close().await;
13746 mock_ctx.close().await;
13747 }
13748
13749 #[tokio::test(flavor = "multi_thread")]
13750 async fn test_new_sketch_uses_unique_variable_name() {
13751 let initial_source = "\
13752sketch1 = sketch(on = XY) {
13753}
13754";
13755
13756 let program = Program::parse(initial_source).unwrap().0.unwrap();
13757
13758 let mut frontend = FrontendState::new();
13759 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13760 let version = Version(0);
13761
13762 frontend.hack_set_program(&ctx, program).await.unwrap();
13763
13764 let sketch_args = SketchCtor {
13765 on: Plane::Default(PlaneName::Yz),
13766 };
13767 let (src_delta, _, _) = frontend
13768 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
13769 .await
13770 .unwrap();
13771
13772 insta::assert_snapshot!("test_new_sketch_uses_unique_variable_name", src_delta.text.as_str());
13773
13774 ctx.close().await;
13775 }
13776
13777 #[tokio::test(flavor = "multi_thread")]
13778 async fn test_new_sketch_twice_using_same_plane() {
13779 let initial_source = "\
13780sketch1 = sketch(on = XY) {
13781}
13782";
13783
13784 let program = Program::parse(initial_source).unwrap().0.unwrap();
13785
13786 let mut frontend = FrontendState::new();
13787 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13788 let version = Version(0);
13789
13790 frontend.hack_set_program(&ctx, program).await.unwrap();
13791
13792 let sketch_args = SketchCtor {
13793 on: Plane::Default(PlaneName::Xy),
13794 };
13795 let (src_delta, _, _) = frontend
13796 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
13797 .await
13798 .unwrap();
13799
13800 insta::assert_snapshot!("test_new_sketch_twice_using_same_plane", src_delta.text.as_str());
13801
13802 ctx.close().await;
13803 }
13804
13805 #[tokio::test(flavor = "multi_thread")]
13806 async fn test_sketch_mode_reuses_cached_on_expression() {
13807 let initial_source = "\
13808width = 2mm
13809sketch(on = offsetPlane(XY, offset = width)) {
13810 line1 = line(start = [var 0, var 0], end = [var 1mm, var 0])
13811 distance([line1.start, line1.end]) == width
13812}
13813";
13814 let program = Program::parse(initial_source).unwrap().0.unwrap();
13815
13816 let mut frontend = FrontendState::new();
13817 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13818 let mock_ctx = ExecutorContext::new_mock(None).await;
13819 let version = Version(0);
13820 let project_id = ProjectId(0);
13821 let file_id = FileId(0);
13822
13823 frontend.hack_set_program(&ctx, program).await.unwrap();
13824 let initial_object_count = frontend.scene_graph.objects.len();
13825 let sketch_id = find_first_sketch_object(&frontend.scene_graph)
13826 .expect("Expected sketch object to exist")
13827 .id;
13828
13829 let scene_delta = frontend
13832 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
13833 .await
13834 .unwrap();
13835 assert_eq!(scene_delta.new_graph.objects.len(), initial_object_count);
13836
13837 let (_src_delta, scene_delta) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
13840 assert_eq!(scene_delta.new_graph.objects.len(), initial_object_count);
13841
13842 ctx.close().await;
13843 mock_ctx.close().await;
13844 }
13845
13846 #[tokio::test(flavor = "multi_thread")]
13847 async fn test_multiple_sketch_blocks() {
13848 let initial_source = "\
13849// Cube that requires the engine.
13850width = 2
13851sketch001 = startSketchOn(XY)
13852profile001 = startProfile(sketch001, at = [0, 0])
13853 |> yLine(length = width, tag = $seg1)
13854 |> xLine(length = width)
13855 |> yLine(length = -width)
13856 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
13857 |> close()
13858extrude001 = extrude(profile001, length = width)
13859
13860// Get a value that requires the engine.
13861x = segLen(seg1)
13862
13863// Triangle with side length 2*x.
13864sketch(on = XY) {
13865 line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
13866 line2 = line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
13867 coincident([line1.end, line2.start])
13868 line3 = line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
13869 coincident([line2.end, line3.start])
13870 coincident([line3.end, line1.start])
13871 equalLength([line3, line1])
13872 equalLength([line1, line2])
13873 distance([line1.start, line1.end]) == 2*x
13874}
13875
13876// Line segment with length x.
13877sketch2 = sketch(on = XY) {
13878 line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
13879 distance([line1.start, line1.end]) == x
13880}
13881";
13882
13883 let program = Program::parse(initial_source).unwrap().0.unwrap();
13884
13885 let mut frontend = FrontendState::new();
13886
13887 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13888 let mock_ctx = ExecutorContext::new_mock(None).await;
13889 let version = Version(0);
13890 let project_id = ProjectId(0);
13891 let file_id = FileId(0);
13892
13893 frontend.hack_set_program(&ctx, program).await.unwrap();
13894 let sketch_objects = frontend
13895 .scene_graph
13896 .objects
13897 .iter()
13898 .filter(|obj| matches!(obj.kind, ObjectKind::Sketch(_)))
13899 .collect::<Vec<_>>();
13900 let sketch1_id = sketch_objects.first().unwrap().id;
13901 let sketch2_id = sketch_objects.get(1).unwrap().id;
13902 let point1_id = ObjectId(sketch1_id.0 + 1);
13904 let point2_id = ObjectId(sketch2_id.0 + 1);
13906
13907 let scene_delta = frontend
13916 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch1_id)
13917 .await
13918 .unwrap();
13919 assert_eq!(
13920 scene_delta.new_graph.objects.len(),
13921 18,
13922 "{:#?}",
13923 scene_delta.new_graph.objects
13924 );
13925
13926 let point_ctor = PointCtor {
13928 position: Point2d {
13929 x: Expr::Var(Number {
13930 value: 1.0,
13931 units: NumericSuffix::Mm,
13932 }),
13933 y: Expr::Var(Number {
13934 value: 2.0,
13935 units: NumericSuffix::Mm,
13936 }),
13937 },
13938 };
13939 let segments = vec![ExistingSegmentCtor {
13940 id: point1_id,
13941 ctor: SegmentCtor::Point(point_ctor),
13942 }];
13943 let (src_delta, _) = frontend
13944 .edit_segments(&mock_ctx, version, sketch1_id, segments)
13945 .await
13946 .unwrap();
13947 insta::assert_snapshot!("test_multiple_sketch_blocks_1", src_delta.text.as_str());
13949 let edited_sketch1_source = src_delta.text.clone();
13950
13951 let (src_delta, _) = frontend.execute_mock(&mock_ctx, version, sketch1_id).await.unwrap();
13953 assert_eq!(src_delta.text, edited_sketch1_source);
13954 let scene = frontend.exit_sketch(&ctx, version, sketch1_id).await.unwrap();
13962 assert_eq!(scene.objects.len(), 30, "{:#?}", scene.objects);
13963
13964 let scene_delta = frontend
13972 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch2_id)
13973 .await
13974 .unwrap();
13975 assert_eq!(
13976 scene_delta.new_graph.objects.len(),
13977 24,
13978 "{:#?}",
13979 scene_delta.new_graph.objects
13980 );
13981
13982 let point_ctor = PointCtor {
13984 position: Point2d {
13985 x: Expr::Var(Number {
13986 value: 3.0,
13987 units: NumericSuffix::Mm,
13988 }),
13989 y: Expr::Var(Number {
13990 value: 4.0,
13991 units: NumericSuffix::Mm,
13992 }),
13993 },
13994 };
13995 let segments = vec![ExistingSegmentCtor {
13996 id: point2_id,
13997 ctor: SegmentCtor::Point(point_ctor),
13998 }];
13999 let (src_delta, _) = frontend
14000 .edit_segments(&mock_ctx, version, sketch2_id, segments)
14001 .await
14002 .unwrap();
14003 insta::assert_snapshot!("test_multiple_sketch_blocks_2", src_delta.text.as_str());
14005 let edited_sketch2_source = src_delta.text.clone();
14006
14007 let (src_delta, _) = frontend.execute_mock(&mock_ctx, version, sketch2_id).await.unwrap();
14009 assert_eq!(src_delta.text, edited_sketch2_source);
14010
14011 ctx.close().await;
14012 mock_ctx.close().await;
14013 }
14014
14015 #[tokio::test(flavor = "multi_thread")]
14016 async fn test_exit_sketch_without_changes_allows_entering_next_sketch() {
14017 clear_mem_cache().await;
14018
14019 let source = r#"sketch001 = sketch(on = XZ) {
14020 circle1 = circle(start = [var -1.96mm, var 2.77mm], center = [var -2.69mm, var 3.44mm])
14021}
14022sketch002 = sketch(on = XY) {
14023 line1 = line(start = [var 0mm, var 0mm], end = [var 4.68mm, var 0mm])
14024 line2 = line(start = [var 4.68mm, var 0mm], end = [var 4.68mm, var 2.96mm])
14025 line3 = line(start = [var 4.68mm, var 2.96mm], end = [var 0mm, var 2.96mm])
14026 line4 = line(start = [var 0mm, var 2.96mm], end = [var 0mm, var 0mm])
14027 coincident([line1.end, line2.start])
14028 coincident([line2.end, line3.start])
14029 coincident([line3.end, line4.start])
14030 coincident([line4.end, line1.start])
14031 parallel([line2, line4])
14032 parallel([line3, line1])
14033 perpendicular([line1, line2])
14034 horizontal(line3)
14035 coincident([line1.start, ORIGIN])
14036}
14037"#;
14038
14039 let program = Program::parse(source).unwrap().0.unwrap();
14040 let mut frontend = FrontendState::new();
14041 let ctx = ExecutorContext::new_with_engine(sync::Arc::new(EngineManager::new_mock()), Default::default());
14042 let mock_ctx = ExecutorContext::new_mock(None).await;
14043 let version = Version(0);
14044 let project_id = ProjectId(0);
14045 let file_id = FileId(0);
14046
14047 frontend.hack_set_program(&ctx, program).await.unwrap();
14048 let sketch_objects = frontend
14049 .scene_graph
14050 .objects
14051 .iter()
14052 .filter(|object| matches!(object.kind, ObjectKind::Sketch(_)))
14053 .collect::<Vec<_>>();
14054 assert_eq!(sketch_objects.len(), 2, "{:#?}", frontend.scene_graph.objects);
14055
14056 let sketch1_id = sketch_objects[0].id;
14057 let sketch2_id = sketch_objects[1].id;
14058
14059 frontend
14060 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch1_id)
14061 .await
14062 .unwrap();
14063 frontend.exit_sketch(&ctx, version, sketch1_id).await.unwrap();
14064
14065 let scene_delta = frontend
14066 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch2_id)
14067 .await
14068 .unwrap();
14069 assert_eq!(scene_delta.new_graph.sketch_mode, Some(sketch2_id));
14070
14071 clear_mem_cache().await;
14072 ctx.close().await;
14073 mock_ctx.close().await;
14074 }
14075
14076 #[tokio::test(flavor = "multi_thread")]
14081 async fn test_extra_newlines_after_settings_edit_sketch_add_point() {
14082 let initial_source = "@settings(defaultLengthUnit = mm)
14084
14085sketch001 = sketch(on = XY) {
14086 point(at = [1in, 2in])
14087}
14088";
14089
14090 let program = Program::parse(initial_source).unwrap().0.unwrap();
14091 let mut frontend = FrontendState::new();
14092
14093 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14094 let mock_ctx = ExecutorContext::new_mock(None).await;
14095 let version = Version(0);
14096 let project_id = ProjectId(0);
14097 let file_id = FileId(0);
14098
14099 frontend.hack_set_program(&ctx, program).await.unwrap();
14100 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14101 let sketch_id = sketch_object.id;
14102
14103 frontend
14105 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
14106 .await
14107 .unwrap();
14108
14109 let point_ctor = PointCtor {
14111 position: Point2d {
14112 x: Expr::Number(Number {
14113 value: 5.0,
14114 units: NumericSuffix::Mm,
14115 }),
14116 y: Expr::Number(Number {
14117 value: 6.0,
14118 units: NumericSuffix::Mm,
14119 }),
14120 },
14121 };
14122 let segment = SegmentCtor::Point(point_ctor);
14123 let (src_delta, scene_delta) = frontend
14124 .add_segment(&mock_ctx, version, sketch_id, segment, None)
14125 .await
14126 .unwrap();
14127 assert!(
14129 src_delta.text.contains("point(at = [5mm, 6mm])"),
14130 "Expected new point in source, got: {}",
14131 src_delta.text
14132 );
14133 assert!(!scene_delta.new_objects.is_empty());
14134
14135 ctx.close().await;
14136 mock_ctx.close().await;
14137 }
14138
14139 #[tokio::test(flavor = "multi_thread")]
14140 async fn test_ensure_control_point_spline_experimental_features_adds_allow_setting() {
14141 let initial_program = Program::parse("s = sketch(on = XY) {}\n").unwrap().0.unwrap();
14142
14143 let updated_program = ensure_control_point_spline_experimental_features(&initial_program).unwrap();
14144 let meta_settings = updated_program.meta_settings().unwrap().unwrap();
14145
14146 assert_eq!(meta_settings.experimental_features, WarningLevel::Allow);
14147 assert!(
14148 source_from_ast(&updated_program.ast).contains("@settings(experimentalFeatures = allow)"),
14149 "Expected experimental settings to be added to source"
14150 );
14151 }
14152
14153 #[tokio::test(flavor = "multi_thread")]
14154 async fn test_extra_newlines_after_settings_add_line_to_empty_sketch() {
14155 let initial_source = "@settings(defaultLengthUnit = mm)
14157
14158s = sketch(on = XY) {}
14159";
14160
14161 let program = Program::parse(initial_source).unwrap().0.unwrap();
14162 let mut frontend = FrontendState::new();
14163
14164 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14165 let mock_ctx = ExecutorContext::new_mock(None).await;
14166 let version = Version(0);
14167
14168 frontend.hack_set_program(&ctx, program).await.unwrap();
14169 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14170 let sketch_id = sketch_object.id;
14171
14172 let line_ctor = LineCtor {
14173 start: Point2d {
14174 x: Expr::Number(Number {
14175 value: 0.0,
14176 units: NumericSuffix::Mm,
14177 }),
14178 y: Expr::Number(Number {
14179 value: 0.0,
14180 units: NumericSuffix::Mm,
14181 }),
14182 },
14183 end: Point2d {
14184 x: Expr::Number(Number {
14185 value: 10.0,
14186 units: NumericSuffix::Mm,
14187 }),
14188 y: Expr::Number(Number {
14189 value: 10.0,
14190 units: NumericSuffix::Mm,
14191 }),
14192 },
14193 construction: None,
14194 };
14195 let segment = SegmentCtor::Line(line_ctor);
14196 let (src_delta, scene_delta) = frontend
14197 .add_segment(&mock_ctx, version, sketch_id, segment, None)
14198 .await
14199 .unwrap();
14200 assert!(
14201 src_delta.text.contains("line(start = [0mm, 0mm], end = [10mm, 10mm])"),
14202 "Expected line in source, got: {}",
14203 src_delta.text
14204 );
14205 assert_eq!(scene_delta.new_objects.len(), 3);
14207
14208 ctx.close().await;
14209 mock_ctx.close().await;
14210 }
14211
14212 #[tokio::test(flavor = "multi_thread")]
14213 async fn test_extra_newlines_between_operations_edit_line() {
14214 let initial_source = "@settings(defaultLengthUnit = mm)
14216
14217sketch001 = sketch(on = XY) {
14218
14219 line1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm])
14220
14221}
14222";
14223
14224 let program = Program::parse(initial_source).unwrap().0.unwrap();
14225 let mut frontend = FrontendState::new();
14226
14227 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14228 let mock_ctx = ExecutorContext::new_mock(None).await;
14229 let version = Version(0);
14230 let project_id = ProjectId(0);
14231 let file_id = FileId(0);
14232
14233 frontend.hack_set_program(&ctx, program).await.unwrap();
14234 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14235 let sketch_id = sketch_object.id;
14236 let sketch = expect_sketch(sketch_object);
14237
14238 let line_id = sketch
14240 .segments
14241 .iter()
14242 .copied()
14243 .find(|seg_id| {
14244 matches!(
14245 &frontend.scene_graph.objects[seg_id.0].kind,
14246 ObjectKind::Segment {
14247 segment: Segment::Line(_)
14248 }
14249 )
14250 })
14251 .expect("Expected a line segment in sketch");
14252
14253 frontend
14255 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
14256 .await
14257 .unwrap();
14258
14259 let line_ctor = LineCtor {
14261 start: Point2d {
14262 x: Expr::Var(Number {
14263 value: 1.0,
14264 units: NumericSuffix::Mm,
14265 }),
14266 y: Expr::Var(Number {
14267 value: 2.0,
14268 units: NumericSuffix::Mm,
14269 }),
14270 },
14271 end: Point2d {
14272 x: Expr::Var(Number {
14273 value: 13.0,
14274 units: NumericSuffix::Mm,
14275 }),
14276 y: Expr::Var(Number {
14277 value: 14.0,
14278 units: NumericSuffix::Mm,
14279 }),
14280 },
14281 construction: None,
14282 };
14283 let segments = vec![ExistingSegmentCtor {
14284 id: line_id,
14285 ctor: SegmentCtor::Line(line_ctor),
14286 }];
14287 let (src_delta, _scene_delta) = frontend
14288 .edit_segments(&mock_ctx, version, sketch_id, segments)
14289 .await
14290 .unwrap();
14291 assert!(
14292 src_delta
14293 .text
14294 .contains("line(start = [var 1mm, var 2mm], end = [var 13mm, var 14mm])"),
14295 "Expected edited line in source, got: {}",
14296 src_delta.text
14297 );
14298
14299 ctx.close().await;
14300 mock_ctx.close().await;
14301 }
14302
14303 #[tokio::test(flavor = "multi_thread")]
14304 async fn test_extra_newlines_delete_segment() {
14305 let initial_source = "@settings(defaultLengthUnit = mm)
14307
14308sketch001 = sketch(on = XY) {
14309 circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
14310}
14311";
14312
14313 let program = Program::parse(initial_source).unwrap().0.unwrap();
14314 let mut frontend = FrontendState::new();
14315
14316 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14317 let mock_ctx = ExecutorContext::new_mock(None).await;
14318 let version = Version(0);
14319
14320 frontend.hack_set_program(&ctx, program).await.unwrap();
14321 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14322 let sketch_id = sketch_object.id;
14323 let sketch = expect_sketch(sketch_object);
14324
14325 assert_eq!(sketch.segments.len(), 3);
14327 let circle_id = sketch.segments[2];
14328
14329 let (src_delta, scene_delta) = frontend
14331 .delete_objects(&mock_ctx, version, sketch_id, vec![], vec![circle_id])
14332 .await
14333 .unwrap();
14334 assert!(
14335 src_delta.text.contains("sketch(on = XY) {"),
14336 "Expected sketch block in source, got: {}",
14337 src_delta.text
14338 );
14339 let new_sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
14340 let new_sketch = expect_sketch(new_sketch_object);
14341 assert_eq!(new_sketch.segments.len(), 0);
14342
14343 ctx.close().await;
14344 mock_ctx.close().await;
14345 }
14346
14347 #[tokio::test(flavor = "multi_thread")]
14348 async fn test_unformatted_source_add_arc() {
14349 let initial_source = "@settings(defaultLengthUnit = mm)
14351
14352sketch001 = sketch(on = XY) {
14353}
14354";
14355
14356 let program = Program::parse(initial_source).unwrap().0.unwrap();
14357 let mut frontend = FrontendState::new();
14358
14359 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14360 let mock_ctx = ExecutorContext::new_mock(None).await;
14361 let version = Version(0);
14362
14363 frontend.hack_set_program(&ctx, program).await.unwrap();
14364 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14365 let sketch_id = sketch_object.id;
14366
14367 let arc_ctor = ArcCtor {
14368 start: Point2d {
14369 x: Expr::Var(Number {
14370 value: 5.0,
14371 units: NumericSuffix::Mm,
14372 }),
14373 y: Expr::Var(Number {
14374 value: 0.0,
14375 units: NumericSuffix::Mm,
14376 }),
14377 },
14378 end: Point2d {
14379 x: Expr::Var(Number {
14380 value: 0.0,
14381 units: NumericSuffix::Mm,
14382 }),
14383 y: Expr::Var(Number {
14384 value: 5.0,
14385 units: NumericSuffix::Mm,
14386 }),
14387 },
14388 center: Point2d {
14389 x: Expr::Var(Number {
14390 value: 0.0,
14391 units: NumericSuffix::Mm,
14392 }),
14393 y: Expr::Var(Number {
14394 value: 0.0,
14395 units: NumericSuffix::Mm,
14396 }),
14397 },
14398 construction: None,
14399 };
14400 let segment = SegmentCtor::Arc(arc_ctor);
14401 let (src_delta, scene_delta) = frontend
14402 .add_segment(&mock_ctx, version, sketch_id, segment, None)
14403 .await
14404 .unwrap();
14405 assert!(
14406 src_delta
14407 .text
14408 .contains("arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])"),
14409 "Expected arc in source, got: {}",
14410 src_delta.text
14411 );
14412 assert!(!scene_delta.new_objects.is_empty());
14413
14414 ctx.close().await;
14415 mock_ctx.close().await;
14416 }
14417
14418 #[tokio::test(flavor = "multi_thread")]
14419 async fn test_extra_newlines_add_circle() {
14420 let initial_source = "@settings(defaultLengthUnit = mm)
14422
14423sketch001 = sketch(on = XY) {
14424}
14425";
14426
14427 let program = Program::parse(initial_source).unwrap().0.unwrap();
14428 let mut frontend = FrontendState::new();
14429
14430 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14431 let mock_ctx = ExecutorContext::new_mock(None).await;
14432 let version = Version(0);
14433
14434 frontend.hack_set_program(&ctx, program).await.unwrap();
14435 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14436 let sketch_id = sketch_object.id;
14437
14438 let circle_ctor = CircleCtor {
14439 start: Point2d {
14440 x: Expr::Var(Number {
14441 value: 5.0,
14442 units: NumericSuffix::Mm,
14443 }),
14444 y: Expr::Var(Number {
14445 value: 0.0,
14446 units: NumericSuffix::Mm,
14447 }),
14448 },
14449 center: Point2d {
14450 x: Expr::Var(Number {
14451 value: 0.0,
14452 units: NumericSuffix::Mm,
14453 }),
14454 y: Expr::Var(Number {
14455 value: 0.0,
14456 units: NumericSuffix::Mm,
14457 }),
14458 },
14459 construction: None,
14460 };
14461 let segment = SegmentCtor::Circle(circle_ctor);
14462 let (src_delta, scene_delta) = frontend
14463 .add_segment(&mock_ctx, version, sketch_id, segment, None)
14464 .await
14465 .unwrap();
14466 assert!(
14467 src_delta
14468 .text
14469 .contains("circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])"),
14470 "Expected circle in source, got: {}",
14471 src_delta.text
14472 );
14473 assert!(!scene_delta.new_objects.is_empty());
14474
14475 ctx.close().await;
14476 mock_ctx.close().await;
14477 }
14478
14479 #[tokio::test(flavor = "multi_thread")]
14480 async fn test_extra_newlines_add_constraint() {
14481 let initial_source = "@settings(defaultLengthUnit = mm)
14483
14484sketch001 = sketch(on = XY) {
14485 line1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm])
14486 line2 = line(start = [var 10mm, var 10mm], end = [var 20mm, var 0mm])
14487}
14488";
14489
14490 let program = Program::parse(initial_source).unwrap().0.unwrap();
14491 let mut frontend = FrontendState::new();
14492
14493 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14494 let mock_ctx = ExecutorContext::new_mock(None).await;
14495 let version = Version(0);
14496 let project_id = ProjectId(0);
14497 let file_id = FileId(0);
14498
14499 frontend.hack_set_program(&ctx, program).await.unwrap();
14500 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14501 let sketch_id = sketch_object.id;
14502 let sketch = expect_sketch(sketch_object);
14503
14504 let line_ids: Vec<ObjectId> = sketch
14506 .segments
14507 .iter()
14508 .copied()
14509 .filter(|seg_id| {
14510 matches!(
14511 &frontend.scene_graph.objects[seg_id.0].kind,
14512 ObjectKind::Segment {
14513 segment: Segment::Line(_)
14514 }
14515 )
14516 })
14517 .collect();
14518 assert_eq!(line_ids.len(), 2, "Expected two line segments");
14519
14520 let line1 = &frontend.scene_graph.objects[line_ids[0].0];
14521 let ObjectKind::Segment {
14522 segment: Segment::Line(line1_data),
14523 } = &line1.kind
14524 else {
14525 panic!("Expected line");
14526 };
14527 let line2 = &frontend.scene_graph.objects[line_ids[1].0];
14528 let ObjectKind::Segment {
14529 segment: Segment::Line(line2_data),
14530 } = &line2.kind
14531 else {
14532 panic!("Expected line");
14533 };
14534
14535 let constraint = Constraint::Coincident(Coincident {
14537 segments: vec![line1_data.end.into(), line2_data.start.into()],
14538 });
14539
14540 frontend
14542 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
14543 .await
14544 .unwrap();
14545 let (src_delta, _scene_delta) = frontend
14546 .add_constraint(&mock_ctx, version, sketch_id, constraint)
14547 .await
14548 .unwrap();
14549 assert!(
14550 src_delta.text.contains("coincident("),
14551 "Expected coincident constraint in source, got: {}",
14552 src_delta.text
14553 );
14554
14555 ctx.close().await;
14556 mock_ctx.close().await;
14557 }
14558
14559 #[tokio::test(flavor = "multi_thread")]
14560 async fn test_extra_newlines_add_line_then_edit_line() {
14561 let initial_source = "@settings(defaultLengthUnit = mm)
14563
14564sketch001 = sketch(on = XY) {
14565}
14566";
14567
14568 let program = Program::parse(initial_source).unwrap().0.unwrap();
14569 let mut frontend = FrontendState::new();
14570
14571 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14572 let mock_ctx = ExecutorContext::new_mock(None).await;
14573 let version = Version(0);
14574
14575 frontend.hack_set_program(&ctx, program).await.unwrap();
14576 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14577 let sketch_id = sketch_object.id;
14578
14579 let line_ctor = LineCtor {
14581 start: Point2d {
14582 x: Expr::Number(Number {
14583 value: 0.0,
14584 units: NumericSuffix::Mm,
14585 }),
14586 y: Expr::Number(Number {
14587 value: 0.0,
14588 units: NumericSuffix::Mm,
14589 }),
14590 },
14591 end: Point2d {
14592 x: Expr::Number(Number {
14593 value: 10.0,
14594 units: NumericSuffix::Mm,
14595 }),
14596 y: Expr::Number(Number {
14597 value: 10.0,
14598 units: NumericSuffix::Mm,
14599 }),
14600 },
14601 construction: None,
14602 };
14603 let segment = SegmentCtor::Line(line_ctor);
14604 let (src_delta, scene_delta) = frontend
14605 .add_segment(&mock_ctx, version, sketch_id, segment, None)
14606 .await
14607 .unwrap();
14608 assert!(
14609 src_delta.text.contains("line(start = [0mm, 0mm], end = [10mm, 10mm])"),
14610 "Expected line in source after add, got: {}",
14611 src_delta.text
14612 );
14613 let line_id = *scene_delta.new_objects.last().unwrap();
14615
14616 let line_ctor = LineCtor {
14618 start: Point2d {
14619 x: Expr::Number(Number {
14620 value: 1.0,
14621 units: NumericSuffix::Mm,
14622 }),
14623 y: Expr::Number(Number {
14624 value: 2.0,
14625 units: NumericSuffix::Mm,
14626 }),
14627 },
14628 end: Point2d {
14629 x: Expr::Number(Number {
14630 value: 13.0,
14631 units: NumericSuffix::Mm,
14632 }),
14633 y: Expr::Number(Number {
14634 value: 14.0,
14635 units: NumericSuffix::Mm,
14636 }),
14637 },
14638 construction: None,
14639 };
14640 let segments = vec![ExistingSegmentCtor {
14641 id: line_id,
14642 ctor: SegmentCtor::Line(line_ctor),
14643 }];
14644 let (src_delta, scene_delta) = frontend
14645 .edit_segments(&mock_ctx, version, sketch_id, segments)
14646 .await
14647 .unwrap();
14648 assert!(
14649 src_delta.text.contains("line(start = [1mm, 2mm], end = [13mm, 14mm])"),
14650 "Expected edited line in source, got: {}",
14651 src_delta.text
14652 );
14653 assert_eq!(scene_delta.new_objects, vec![]);
14654
14655 ctx.close().await;
14656 mock_ctx.close().await;
14657 }
14658}