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, errors) = Program::parse(&new_source)
645 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
646 if !errors.is_empty() {
647 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
648 "Error parsing KCL source after adding sketch: {errors:?}"
649 ))));
650 }
651 let Some(new_program) = new_program else {
652 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
653 "No AST produced after adding sketch".to_owned(),
654 )));
655 };
656
657 self.program = new_program.clone();
659
660 let outcome = ctx.run_with_caching(new_program.clone()).await?;
663 let freedom_analysis_ran = true;
664
665 let outcome = self.update_state_after_exec(outcome, freedom_analysis_ran);
666
667 let Some(sketch_id) = self
668 .scene_graph
669 .objects
670 .iter()
671 .filter_map(|object| match object.kind {
672 ObjectKind::Sketch(_) => Some(object.id),
673 _ => None,
674 })
675 .max_by_key(|id| id.0)
676 else {
677 return Err(KclErrorWithOutputs::from_error_outcome(
678 KclError::refactor("No objects in scene graph after adding sketch".to_owned()),
679 outcome,
680 ));
681 };
682 self.scene_graph.sketch_mode = Some(sketch_id);
684
685 let src_delta = SourceDelta { text: new_source };
686 let scene_graph_delta = SceneGraphDelta {
687 new_graph: self.scene_graph_for_ui(),
688 invalidates_ids: false,
689 new_objects: vec![sketch_id],
690 exec_outcome: outcome,
691 };
692 Ok((src_delta, scene_graph_delta, sketch_id))
693 }
694
695 async fn edit_sketch(
696 &mut self,
697 ctx: &ExecutorContext,
698 _project: ProjectId,
699 _file: FileId,
700 _version: Version,
701 sketch: ObjectId,
702 ) -> ExecResult<SceneGraphDelta> {
703 let sketch_object = self.scene_graph.objects.get(sketch.0).ok_or_else(|| {
707 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
708 })?;
709 let ObjectKind::Sketch(_) = &sketch_object.kind else {
710 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
711 "Object is not a sketch, it is {}",
712 sketch_object.kind.human_friendly_kind_with_article()
713 ))));
714 };
715 let sketch_block_ref = expect_single_node_ref(sketch_object).map_err(KclErrorWithOutputs::no_outputs)?;
716
717 self.scene_graph.sketch_mode = Some(sketch);
719
720 let mut truncated_program = self.program.clone();
722 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::None)
723 .map_err(KclErrorWithOutputs::no_outputs)?;
724
725 let outcome = ctx
728 .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch))
729 .await?;
730
731 let outcome = self.update_state_after_exec(outcome, true);
733 let scene_graph_delta = SceneGraphDelta {
734 new_graph: self.scene_graph_for_ui(),
735 invalidates_ids: false,
736 new_objects: Vec::new(),
737 exec_outcome: outcome,
738 };
739 Ok(scene_graph_delta)
740 }
741
742 async fn exit_sketch(
743 &mut self,
744 ctx: &ExecutorContext,
745 _version: Version,
746 sketch: ObjectId,
747 ) -> ExecResult<SceneGraph> {
748 #[cfg(not(target_arch = "wasm32"))]
750 let _ = sketch;
751 #[cfg(target_arch = "wasm32")]
752 if self.scene_graph.sketch_mode != Some(sketch) {
753 web_sys::console::warn_1(
754 &format!(
755 "WARNING: exit_sketch: current state's sketch mode ID doesn't match the given sketch ID; state={:#?}, given={sketch:?}",
756 &self.scene_graph.sketch_mode
757 )
758 .into(),
759 );
760 }
761 self.scene_graph.sketch_mode = None;
762
763 let outcome = ctx.run_with_caching(self.program.clone()).await?;
765
766 self.update_state_after_exec(outcome, false);
768
769 Ok(self.scene_graph_for_ui())
770 }
771
772 async fn delete_sketch(
773 &mut self,
774 ctx: &ExecutorContext,
775 _version: Version,
776 sketch: ObjectId,
777 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
778 let mut new_ast = self.program.ast.clone();
781
782 let sketch_id = sketch;
784 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
785 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
786 })?;
787 let ObjectKind::Sketch(_) = &sketch_object.kind else {
788 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
789 "Object is not a sketch, it is {}",
790 sketch_object.kind.human_friendly_kind_with_article(),
791 ))));
792 };
793
794 self.mutate_ast(&mut new_ast, sketch_id, AstMutateCommand::DeleteNode)
796 .map_err(KclErrorWithOutputs::no_outputs)?;
797
798 self.execute_after_delete_sketch(ctx, &mut new_ast).await
799 }
800
801 async fn add_segment(
802 &mut self,
803 ctx: &ExecutorContext,
804 _version: Version,
805 sketch: ObjectId,
806 segment: SegmentCtor,
807 _label: Option<String>,
808 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
809 match segment {
811 SegmentCtor::Point(ctor) => self.add_point(ctx, sketch, ctor).await,
812 SegmentCtor::Line(ctor) => self.add_line(ctx, sketch, ctor).await,
813 SegmentCtor::Arc(ctor) => self.add_arc(ctx, sketch, ctor).await,
814 SegmentCtor::Circle(ctor) => self.add_circle(ctx, sketch, ctor).await,
815 SegmentCtor::ControlPointSpline(ctor) => self.add_control_point_spline(ctx, sketch, ctor).await,
816 }
817 }
818
819 async fn edit_segments(
820 &mut self,
821 ctx: &ExecutorContext,
822 _version: Version,
823 sketch: ObjectId,
824 segments: Vec<ExistingSegmentCtor>,
825 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
826 let sketch_block_ref =
828 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
829
830 let mut new_ast = self.program.ast.clone();
831 let mut edited_segment_ids = AhashIndexSet::with_capacity_and_hasher(segments.len(), Default::default());
832 let mut invalidates_ids = false;
833
834 for segment in &segments {
837 edited_segment_ids.insert(segment.id);
838 if let SegmentCtor::ControlPointSpline(new_ctor) = &segment.ctor
839 && let Some(existing_object) = self.scene_graph.objects.get(segment.id.0)
840 && let ObjectKind::Segment {
841 segment: Segment::ControlPointSpline(existing_spline),
842 } = &existing_object.kind
843 && existing_spline.controls.len() != new_ctor.points.len()
844 {
845 invalidates_ids = true;
846 }
847 }
848 let drag_anchor_segment_ids = self
849 .next_drag_anchor_segment_ids
850 .take()
851 .unwrap_or_else(|| edited_segment_ids.clone());
852 let commit_solved_initial_guesses = self.next_edit_commits_solver_solutions.take().unwrap_or(true);
853
854 let mut final_edits: IndexMap<ObjectId, SegmentCtor> = IndexMap::new();
869
870 for segment in segments {
871 let segment_id = segment.id;
872 match segment.ctor {
873 SegmentCtor::Point(ctor) => {
874 if let Some(segment_object) = self.scene_graph.objects.get(segment_id.0)
876 && let ObjectKind::Segment { segment } = &segment_object.kind
877 && let Segment::Point(point) = segment
878 && let Some(owner_id) = point.owner
879 && let Some(owner_object) = self.scene_graph.objects.get(owner_id.0)
880 && let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind
881 {
882 match owner_segment {
883 Segment::Line(line) if line.start == segment_id || line.end == segment_id => {
884 if let Some(existing) = final_edits.get_mut(&owner_id) {
885 let SegmentCtor::Line(line_ctor) = existing else {
886 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
887 "Internal: Expected line ctor for owner, but found {}",
888 existing.human_friendly_kind_with_article()
889 ))));
890 };
891 if line.start == segment_id {
893 line_ctor.start = ctor.position;
894 } else {
895 line_ctor.end = ctor.position;
896 }
897 } else if let SegmentCtor::Line(line_ctor) = &line.ctor {
898 let mut line_ctor = line_ctor.clone();
900 if line.start == segment_id {
901 line_ctor.start = ctor.position;
902 } else {
903 line_ctor.end = ctor.position;
904 }
905 final_edits.insert(owner_id, SegmentCtor::Line(line_ctor));
906 } else {
907 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
909 "Internal: Line does not have line ctor, but found {}",
910 line.ctor.human_friendly_kind_with_article()
911 ))));
912 }
913 continue;
914 }
915 Segment::Arc(arc)
916 if arc.start == segment_id || arc.end == segment_id || arc.center == segment_id =>
917 {
918 if let Some(existing) = final_edits.get_mut(&owner_id) {
919 let SegmentCtor::Arc(arc_ctor) = existing else {
920 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
921 "Internal: Expected arc ctor for owner, but found {}",
922 existing.human_friendly_kind_with_article()
923 ))));
924 };
925 if arc.start == segment_id {
926 arc_ctor.start = ctor.position;
927 } else if arc.end == segment_id {
928 arc_ctor.end = ctor.position;
929 } else {
930 arc_ctor.center = ctor.position;
931 }
932 } else if let SegmentCtor::Arc(arc_ctor) = &arc.ctor {
933 let mut arc_ctor = arc_ctor.clone();
934 if arc.start == segment_id {
935 arc_ctor.start = ctor.position;
936 } else if arc.end == segment_id {
937 arc_ctor.end = ctor.position;
938 } else {
939 arc_ctor.center = ctor.position;
940 }
941 final_edits.insert(owner_id, SegmentCtor::Arc(arc_ctor));
942 } else {
943 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
944 "Internal: Arc does not have arc ctor, but found {}",
945 arc.ctor.human_friendly_kind_with_article()
946 ))));
947 }
948 continue;
949 }
950 Segment::Circle(circle) if circle.start == segment_id || circle.center == segment_id => {
951 if let Some(existing) = final_edits.get_mut(&owner_id) {
952 let SegmentCtor::Circle(circle_ctor) = existing else {
953 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
954 "Internal: Expected circle ctor for owner, but found {}",
955 existing.human_friendly_kind_with_article()
956 ))));
957 };
958 if circle.start == segment_id {
959 circle_ctor.start = ctor.position;
960 } else {
961 circle_ctor.center = ctor.position;
962 }
963 } else if let SegmentCtor::Circle(circle_ctor) = &circle.ctor {
964 let mut circle_ctor = circle_ctor.clone();
965 if circle.start == segment_id {
966 circle_ctor.start = ctor.position;
967 } else {
968 circle_ctor.center = ctor.position;
969 }
970 final_edits.insert(owner_id, SegmentCtor::Circle(circle_ctor));
971 } else {
972 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
973 "Internal: Circle does not have circle ctor, but found {}",
974 circle.ctor.human_friendly_kind_with_article()
975 ))));
976 }
977 continue;
978 }
979 Segment::ControlPointSpline(spline) if spline.controls.contains(&segment_id) => {
980 let Some(control_index) =
981 spline.controls.iter().position(|control_id| *control_id == segment_id)
982 else {
983 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
984 "Internal: Point is not part of owner's controlPointSpline segment: point={segment_id:?}, spline={owner_id:?}"
985 ))));
986 };
987 if let Some(existing) = final_edits.get_mut(&owner_id) {
988 let SegmentCtor::ControlPointSpline(spline_ctor) = existing else {
989 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
990 "Internal: Expected controlPointSpline ctor for owner, but found {}",
991 existing.human_friendly_kind_with_article()
992 ))));
993 };
994 spline_ctor.points[control_index] = ctor.position;
995 } else if let SegmentCtor::ControlPointSpline(spline_ctor) = &spline.ctor {
996 let mut spline_ctor = spline_ctor.clone();
997 spline_ctor.points[control_index] = ctor.position;
998 final_edits.insert(owner_id, SegmentCtor::ControlPointSpline(spline_ctor));
999 } else {
1000 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1001 "Internal: Control point spline does not have controlPointSpline ctor, but found {}",
1002 spline.ctor.human_friendly_kind_with_article()
1003 ))));
1004 }
1005 continue;
1006 }
1007 _ => {}
1008 }
1009 }
1010
1011 final_edits.insert(segment_id, SegmentCtor::Point(ctor));
1013 }
1014 SegmentCtor::Line(ctor) => {
1015 final_edits.insert(segment_id, SegmentCtor::Line(ctor));
1016 }
1017 SegmentCtor::Arc(ctor) => {
1018 final_edits.insert(segment_id, SegmentCtor::Arc(ctor));
1019 }
1020 SegmentCtor::Circle(ctor) => {
1021 final_edits.insert(segment_id, SegmentCtor::Circle(ctor));
1022 }
1023 SegmentCtor::ControlPointSpline(ctor) => {
1024 final_edits.insert(segment_id, SegmentCtor::ControlPointSpline(ctor));
1025 }
1026 }
1027 }
1028
1029 for (segment_id, ctor) in final_edits {
1030 match ctor {
1031 SegmentCtor::Point(ctor) => self
1032 .edit_point(&mut new_ast, sketch, segment_id, ctor)
1033 .map_err(KclErrorWithOutputs::no_outputs)?,
1034 SegmentCtor::Line(ctor) => self
1035 .edit_line(&mut new_ast, sketch, segment_id, ctor)
1036 .map_err(KclErrorWithOutputs::no_outputs)?,
1037 SegmentCtor::Arc(ctor) => self
1038 .edit_arc(&mut new_ast, sketch, segment_id, ctor)
1039 .map_err(KclErrorWithOutputs::no_outputs)?,
1040 SegmentCtor::Circle(ctor) => self
1041 .edit_circle(&mut new_ast, sketch, segment_id, ctor)
1042 .map_err(KclErrorWithOutputs::no_outputs)?,
1043 SegmentCtor::ControlPointSpline(ctor) => self
1044 .edit_control_point_spline(&mut new_ast, sketch, segment_id, ctor)
1045 .map_err(KclErrorWithOutputs::no_outputs)?,
1046 }
1047 }
1048 let (source_delta, mut scene_graph_delta) = self
1049 .execute_after_edit(
1050 ctx,
1051 sketch,
1052 sketch_block_ref,
1053 &mut new_ast,
1054 ExecuteAfterEditOptions {
1055 segment_ids_edited: drag_anchor_segment_ids,
1056 edit_kind: EditDeleteKind::Edit,
1057 commit_solved_initial_guesses,
1058 },
1059 )
1060 .await?;
1061 if invalidates_ids {
1062 scene_graph_delta.invalidates_ids = true;
1063 }
1064 Ok((source_delta, scene_graph_delta))
1065 }
1066
1067 async fn delete_objects(
1068 &mut self,
1069 ctx: &ExecutorContext,
1070 _version: Version,
1071 sketch: ObjectId,
1072 constraint_ids: Vec<ObjectId>,
1073 segment_ids: Vec<ObjectId>,
1074 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1075 let sketch_block_ref =
1077 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1078
1079 let mut constraint_ids_set = constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
1081 let segment_ids_set = segment_ids.into_iter().collect::<AhashIndexSet<_>>();
1082
1083 let mut resolved_segment_ids_to_delete = AhashIndexSet::default();
1086
1087 for segment_id in segment_ids_set.iter().copied() {
1088 let owner_id = self.scene_graph.objects.get(segment_id.0).and_then(|segment_object| {
1089 let ObjectKind::Segment { segment } = &segment_object.kind else {
1090 return None;
1091 };
1092 match segment {
1093 Segment::Point(point) => point.owner,
1094 Segment::Line(line) => line.owner,
1095 _ => None,
1096 }
1097 });
1098
1099 if let Some(owner_id) = owner_id
1100 && let Some(owner_object) = self.scene_graph.objects.get(owner_id.0)
1101 && let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind
1102 && matches!(
1103 owner_segment,
1104 Segment::Line(_) | Segment::Arc(_) | Segment::Circle(_) | Segment::ControlPointSpline(_)
1105 )
1106 {
1107 resolved_segment_ids_to_delete.insert(owner_id);
1109 } else {
1110 resolved_segment_ids_to_delete.insert(segment_id);
1112 }
1113 }
1114 let referenced_constraint_ids = self
1115 .find_referenced_constraints(sketch, &resolved_segment_ids_to_delete)
1116 .map_err(KclErrorWithOutputs::no_outputs)?;
1117
1118 let mut new_ast = self.program.ast.clone();
1119
1120 for constraint_id in referenced_constraint_ids {
1121 if constraint_ids_set.contains(&constraint_id) {
1122 continue;
1123 }
1124
1125 let constraint_object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
1126 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Constraint not found: {constraint_id:?}")))
1127 })?;
1128 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
1129 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1130 "Object is not a constraint, it is {}",
1131 constraint_object.kind.human_friendly_kind_with_article()
1132 ))));
1133 };
1134
1135 match constraint {
1136 Constraint::Coincident(coincident) => {
1137 let remaining_segments =
1138 self.remaining_constraint_segments(&coincident.segments, &resolved_segment_ids_to_delete);
1139
1140 if remaining_segments.len() >= 2 {
1142 self.edit_coincident_constraint(&mut new_ast, constraint_id, remaining_segments)
1143 .map_err(KclErrorWithOutputs::no_outputs)?;
1144 } else {
1145 constraint_ids_set.insert(constraint_id);
1146 }
1147 }
1148 Constraint::EqualRadius(equal_radius) => {
1149 let remaining_input = equal_radius
1150 .input
1151 .iter()
1152 .copied()
1153 .filter(|segment_id| {
1154 !self.segment_will_be_deleted(*segment_id, &resolved_segment_ids_to_delete)
1155 })
1156 .collect::<Vec<_>>();
1157
1158 if remaining_input.len() >= 2 {
1159 self.edit_equal_radius_constraint(&mut new_ast, constraint_id, remaining_input)
1160 .map_err(KclErrorWithOutputs::no_outputs)?;
1161 } else {
1162 constraint_ids_set.insert(constraint_id);
1163 }
1164 }
1165 Constraint::LinesEqualLength(lines_equal_length) => {
1166 let remaining_lines = lines_equal_length
1167 .lines
1168 .iter()
1169 .copied()
1170 .filter(|line_id| !self.segment_will_be_deleted(*line_id, &resolved_segment_ids_to_delete))
1171 .collect::<Vec<_>>();
1172
1173 if remaining_lines.len() >= 2 {
1175 self.edit_equal_length_constraint(&mut new_ast, constraint_id, remaining_lines)
1176 .map_err(KclErrorWithOutputs::no_outputs)?;
1177 } else {
1178 constraint_ids_set.insert(constraint_id);
1179 }
1180 }
1181 Constraint::Parallel(parallel) => {
1182 let remaining_lines = parallel
1183 .lines
1184 .iter()
1185 .copied()
1186 .filter(|line_id| !self.segment_will_be_deleted(*line_id, &resolved_segment_ids_to_delete))
1187 .collect::<Vec<_>>();
1188
1189 if remaining_lines.len() >= 2 {
1190 self.edit_parallel_constraint(&mut new_ast, constraint_id, remaining_lines)
1191 .map_err(KclErrorWithOutputs::no_outputs)?;
1192 } else {
1193 constraint_ids_set.insert(constraint_id);
1194 }
1195 }
1196 Constraint::Horizontal(Horizontal::Points { points }) => {
1197 let remaining_points = self.remaining_constraint_segments(points, &resolved_segment_ids_to_delete);
1198
1199 if remaining_points.len() >= 2 {
1200 self.edit_horizontal_points_constraint(&mut new_ast, constraint_id, remaining_points)
1201 .map_err(KclErrorWithOutputs::no_outputs)?;
1202 } else {
1203 constraint_ids_set.insert(constraint_id);
1204 }
1205 }
1206 Constraint::Vertical(Vertical::Points { points }) => {
1207 let remaining_points = self.remaining_constraint_segments(points, &resolved_segment_ids_to_delete);
1208
1209 if remaining_points.len() >= 2 {
1210 self.edit_vertical_points_constraint(&mut new_ast, constraint_id, remaining_points)
1211 .map_err(KclErrorWithOutputs::no_outputs)?;
1212 } else {
1213 constraint_ids_set.insert(constraint_id);
1214 }
1215 }
1216 Constraint::Fixed(fixed) => {
1217 if fixed.points.iter().any(|fixed_point| {
1218 self.segment_will_be_deleted(fixed_point.point, &resolved_segment_ids_to_delete)
1219 }) {
1220 constraint_ids_set.insert(constraint_id);
1221 }
1222 }
1223 _ => {
1224 constraint_ids_set.insert(constraint_id);
1226 }
1227 }
1228 }
1229
1230 for constraint_id in constraint_ids_set {
1231 self.delete_constraint(&mut new_ast, sketch, constraint_id)
1232 .map_err(KclErrorWithOutputs::no_outputs)?;
1233 }
1234 for segment_id in resolved_segment_ids_to_delete {
1235 self.delete_segment(&mut new_ast, sketch, segment_id)
1236 .map_err(KclErrorWithOutputs::no_outputs)?;
1237 }
1238
1239 self.execute_after_edit(
1240 ctx,
1241 sketch,
1242 sketch_block_ref,
1243 &mut new_ast,
1244 ExecuteAfterEditOptions {
1245 segment_ids_edited: Default::default(),
1246 edit_kind: EditDeleteKind::DeleteNonSketch,
1247 commit_solved_initial_guesses: true,
1248 },
1249 )
1250 .await
1251 }
1252
1253 async fn add_constraint(
1254 &mut self,
1255 ctx: &ExecutorContext,
1256 _version: Version,
1257 sketch: ObjectId,
1258 constraint: Constraint,
1259 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1260 let original_program = self.program.clone();
1264 let original_scene_graph = self.scene_graph.clone();
1265
1266 let mut new_ast = self.program.ast.clone();
1267 let sketch_block_ref = match constraint {
1268 Constraint::Coincident(coincident) => self
1269 .add_coincident(sketch, coincident, &mut new_ast)
1270 .await
1271 .map_err(KclErrorWithOutputs::no_outputs)?,
1272 Constraint::Distance(distance) => self
1273 .add_distance(sketch, distance, &mut new_ast)
1274 .await
1275 .map_err(KclErrorWithOutputs::no_outputs)?,
1276 Constraint::EqualRadius(equal_radius) => self
1277 .add_equal_radius(sketch, equal_radius, &mut new_ast)
1278 .await
1279 .map_err(KclErrorWithOutputs::no_outputs)?,
1280 Constraint::Fixed(fixed) => self
1281 .add_fixed_constraints(sketch, fixed.points, &mut new_ast)
1282 .await
1283 .map_err(KclErrorWithOutputs::no_outputs)?,
1284 Constraint::HorizontalDistance(distance) => self
1285 .add_horizontal_distance(sketch, distance, &mut new_ast)
1286 .await
1287 .map_err(KclErrorWithOutputs::no_outputs)?,
1288 Constraint::VerticalDistance(distance) => self
1289 .add_vertical_distance(sketch, distance, &mut new_ast)
1290 .await
1291 .map_err(KclErrorWithOutputs::no_outputs)?,
1292 Constraint::Horizontal(horizontal) => self
1293 .add_horizontal(sketch, horizontal, &mut new_ast)
1294 .await
1295 .map_err(KclErrorWithOutputs::no_outputs)?,
1296 Constraint::LinesEqualLength(lines_equal_length) => self
1297 .add_lines_equal_length(sketch, lines_equal_length, &mut new_ast)
1298 .await
1299 .map_err(KclErrorWithOutputs::no_outputs)?,
1300 Constraint::Midpoint(midpoint) => self
1301 .add_midpoint(sketch, midpoint, &mut new_ast)
1302 .await
1303 .map_err(KclErrorWithOutputs::no_outputs)?,
1304 Constraint::Parallel(parallel) => self
1305 .add_parallel(sketch, parallel, &mut new_ast)
1306 .await
1307 .map_err(KclErrorWithOutputs::no_outputs)?,
1308 Constraint::Perpendicular(perpendicular) => self
1309 .add_perpendicular(sketch, perpendicular, &mut new_ast)
1310 .await
1311 .map_err(KclErrorWithOutputs::no_outputs)?,
1312 Constraint::Radius(radius) => self
1313 .add_radius(sketch, radius, &mut new_ast)
1314 .await
1315 .map_err(KclErrorWithOutputs::no_outputs)?,
1316 Constraint::Diameter(diameter) => self
1317 .add_diameter(sketch, diameter, &mut new_ast)
1318 .await
1319 .map_err(KclErrorWithOutputs::no_outputs)?,
1320 Constraint::Symmetric(symmetric) => self
1321 .add_symmetric(sketch, symmetric, &mut new_ast)
1322 .await
1323 .map_err(KclErrorWithOutputs::no_outputs)?,
1324 Constraint::Vertical(vertical) => self
1325 .add_vertical(sketch, vertical, &mut new_ast)
1326 .await
1327 .map_err(KclErrorWithOutputs::no_outputs)?,
1328 Constraint::Angle(lines_at_angle) => self
1329 .add_angle(sketch, lines_at_angle, &mut new_ast)
1330 .await
1331 .map_err(KclErrorWithOutputs::no_outputs)?,
1332 Constraint::Tangent(tangent) => self
1333 .add_tangent(sketch, tangent, &mut new_ast)
1334 .await
1335 .map_err(KclErrorWithOutputs::no_outputs)?,
1336 };
1337
1338 let result = self
1339 .execute_after_add_constraint(ctx, sketch, sketch_block_ref, &mut new_ast)
1340 .await;
1341
1342 if result.is_err() {
1344 self.program = original_program;
1345 self.scene_graph = original_scene_graph;
1346 }
1347
1348 result
1349 }
1350
1351 async fn chain_segment(
1352 &mut self,
1353 ctx: &ExecutorContext,
1354 version: Version,
1355 sketch: ObjectId,
1356 previous_segment_end_point_id: ObjectId,
1357 segment: SegmentCtor,
1358 _label: Option<String>,
1359 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1360 let SegmentCtor::Line(line_ctor) = segment else {
1364 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1365 "chain_segment currently only supports Line segments, got {}",
1366 segment.human_friendly_kind_with_article(),
1367 ))));
1368 };
1369
1370 let (_first_src_delta, first_scene_delta) = self.add_line(ctx, sketch, line_ctor).await?;
1372
1373 let new_line_id = first_scene_delta
1376 .new_objects
1377 .iter()
1378 .find(|&obj_id| {
1379 let obj = self.scene_graph.objects.get(obj_id.0);
1380 if let Some(obj) = obj {
1381 matches!(
1382 &obj.kind,
1383 ObjectKind::Segment {
1384 segment: Segment::Line(_)
1385 }
1386 )
1387 } else {
1388 false
1389 }
1390 })
1391 .ok_or_else(|| {
1392 KclErrorWithOutputs::no_outputs(KclError::refactor(
1393 "Failed to find new line segment in scene graph".to_string(),
1394 ))
1395 })?;
1396
1397 let new_line_obj = self.scene_graph.objects.get(new_line_id.0).ok_or_else(|| {
1398 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1399 "New line object not found: {new_line_id:?}"
1400 )))
1401 })?;
1402
1403 let ObjectKind::Segment {
1404 segment: new_line_segment,
1405 } = &new_line_obj.kind
1406 else {
1407 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1408 "Object is not a segment: {new_line_obj:?}"
1409 ))));
1410 };
1411
1412 let Segment::Line(new_line) = new_line_segment else {
1413 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1414 "Segment is not a line: {new_line_segment:?}"
1415 ))));
1416 };
1417
1418 let new_line_start_point_id = new_line.start;
1419
1420 let coincident = Coincident {
1422 segments: vec![previous_segment_end_point_id.into(), new_line_start_point_id.into()],
1423 };
1424
1425 let (final_src_delta, final_scene_delta) = self
1426 .add_constraint(ctx, version, sketch, Constraint::Coincident(coincident))
1427 .await?;
1428
1429 let mut combined_new_objects = first_scene_delta.new_objects.clone();
1432 combined_new_objects.extend(final_scene_delta.new_objects);
1433
1434 let scene_graph_delta = SceneGraphDelta {
1435 new_graph: self.scene_graph_for_ui(),
1436 invalidates_ids: false,
1437 new_objects: combined_new_objects,
1438 exec_outcome: final_scene_delta.exec_outcome,
1439 };
1440
1441 Ok((final_src_delta, scene_graph_delta))
1442 }
1443
1444 async fn edit_constraint(
1445 &mut self,
1446 ctx: &ExecutorContext,
1447 _version: Version,
1448 sketch: ObjectId,
1449 constraint_id: ObjectId,
1450 value_expression: String,
1451 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1452 let sketch_block_ref =
1454 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1455
1456 let object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
1457 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Object not found: {constraint_id:?}")))
1458 })?;
1459 if !matches!(&object.kind, ObjectKind::Constraint { .. }) {
1460 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1461 "Object is not a constraint: {constraint_id:?}"
1462 ))));
1463 }
1464
1465 let mut new_ast = self.program.ast.clone();
1466
1467 let (parsed, errors) = Program::parse(&value_expression)
1469 .map_err(|e| KclErrorWithOutputs::no_outputs(KclError::refactor(e.to_string())))?;
1470 if !errors.is_empty() {
1471 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1472 "Error parsing value expression: {errors:?}"
1473 ))));
1474 }
1475 let mut parsed = parsed.ok_or_else(|| {
1476 KclErrorWithOutputs::no_outputs(KclError::refactor("No AST produced from value expression".to_string()))
1477 })?;
1478 if parsed.ast.body.is_empty() {
1479 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1480 "Empty value expression".to_string(),
1481 )));
1482 }
1483 let first = parsed.ast.body.remove(0);
1484 let ast::BodyItem::ExpressionStatement(expr_stmt) = first else {
1485 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1486 "Value expression must be a simple expression".to_string(),
1487 )));
1488 };
1489
1490 let new_value: ast::BinaryPart = expr_stmt
1491 .inner
1492 .expression
1493 .try_into()
1494 .map_err(|e: String| KclErrorWithOutputs::no_outputs(KclError::refactor(e)))?;
1495
1496 self.mutate_ast(
1497 &mut new_ast,
1498 constraint_id,
1499 AstMutateCommand::EditConstraintValue { value: new_value },
1500 )
1501 .map_err(KclErrorWithOutputs::no_outputs)?;
1502
1503 self.execute_after_edit(
1504 ctx,
1505 sketch,
1506 sketch_block_ref,
1507 &mut new_ast,
1508 ExecuteAfterEditOptions {
1509 segment_ids_edited: Default::default(),
1510 edit_kind: EditDeleteKind::Edit,
1511 commit_solved_initial_guesses: true,
1512 },
1513 )
1514 .await
1515 }
1516
1517 async fn edit_distance_constraint_label_position(
1518 &mut self,
1519 ctx: &ExecutorContext,
1520 _version: Version,
1521 sketch: ObjectId,
1522 constraint_id: ObjectId,
1523 label_position: Point2d<Number>,
1524 anchor_segment_ids: Vec<ObjectId>,
1525 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1526 let sketch_block_ref =
1528 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1529
1530 let object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
1531 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Object not found: {constraint_id:?}")))
1532 })?;
1533 if !matches!(
1534 &object.kind,
1535 ObjectKind::Constraint {
1536 constraint: Constraint::Distance(_)
1537 | Constraint::HorizontalDistance(_)
1538 | Constraint::VerticalDistance(_)
1539 | Constraint::Radius(_)
1540 | Constraint::Diameter(_),
1541 }
1542 ) {
1543 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1544 "Object does not support labelPosition: {constraint_id:?}"
1545 ))));
1546 }
1547
1548 let label_position = to_ast_point2d_number(&label_position).map_err(|err| {
1549 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1550 "Could not convert label position to AST: {err}"
1551 )))
1552 })?;
1553 let mut new_ast = self.program.ast.clone();
1554 self.mutate_ast(
1555 &mut new_ast,
1556 constraint_id,
1557 AstMutateCommand::EditDistanceConstraintLabelPosition { label_position },
1558 )
1559 .map_err(KclErrorWithOutputs::no_outputs)?;
1560 let commit_solved_initial_guesses = self.next_edit_commits_solver_solutions.take().unwrap_or(true);
1561
1562 self.execute_after_edit(
1563 ctx,
1564 sketch,
1565 sketch_block_ref,
1566 &mut new_ast,
1567 ExecuteAfterEditOptions {
1568 segment_ids_edited: anchor_segment_ids.into_iter().collect(),
1569 edit_kind: EditDeleteKind::Edit,
1570 commit_solved_initial_guesses,
1571 },
1572 )
1573 .await
1574 }
1575
1576 async fn batch_split_segment_operations(
1584 &mut self,
1585 ctx: &ExecutorContext,
1586 _version: Version,
1587 sketch: ObjectId,
1588 edit_segments: Vec<ExistingSegmentCtor>,
1589 add_constraints: Vec<Constraint>,
1590 delete_constraint_ids: Vec<ObjectId>,
1591 _new_segment_info: sketch::NewSegmentInfo,
1592 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1593 let sketch_block_ref =
1595 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1596
1597 let mut new_ast = self.program.ast.clone();
1598 let mut segment_ids_edited = AhashIndexSet::with_capacity_and_hasher(edit_segments.len(), Default::default());
1599
1600 for segment in edit_segments {
1602 segment_ids_edited.insert(segment.id);
1603 match segment.ctor {
1604 SegmentCtor::Point(ctor) => self
1605 .edit_point(&mut new_ast, sketch, segment.id, ctor)
1606 .map_err(KclErrorWithOutputs::no_outputs)?,
1607 SegmentCtor::Line(ctor) => self
1608 .edit_line(&mut new_ast, sketch, segment.id, ctor)
1609 .map_err(KclErrorWithOutputs::no_outputs)?,
1610 SegmentCtor::Arc(ctor) => self
1611 .edit_arc(&mut new_ast, sketch, segment.id, ctor)
1612 .map_err(KclErrorWithOutputs::no_outputs)?,
1613 SegmentCtor::Circle(ctor) => self
1614 .edit_circle(&mut new_ast, sketch, segment.id, ctor)
1615 .map_err(KclErrorWithOutputs::no_outputs)?,
1616 SegmentCtor::ControlPointSpline(ctor) => self
1617 .edit_control_point_spline(&mut new_ast, sketch, segment.id, ctor)
1618 .map_err(KclErrorWithOutputs::no_outputs)?,
1619 }
1620 }
1621
1622 for constraint in add_constraints {
1624 match constraint {
1625 Constraint::Coincident(coincident) => {
1626 self.add_coincident(sketch, coincident, &mut new_ast)
1627 .await
1628 .map_err(KclErrorWithOutputs::no_outputs)?;
1629 }
1630 Constraint::Distance(distance) => {
1631 self.add_distance(sketch, distance, &mut new_ast)
1632 .await
1633 .map_err(KclErrorWithOutputs::no_outputs)?;
1634 }
1635 Constraint::EqualRadius(equal_radius) => {
1636 self.add_equal_radius(sketch, equal_radius, &mut new_ast)
1637 .await
1638 .map_err(KclErrorWithOutputs::no_outputs)?;
1639 }
1640 Constraint::Fixed(fixed) => {
1641 self.add_fixed_constraints(sketch, fixed.points, &mut new_ast)
1642 .await
1643 .map_err(KclErrorWithOutputs::no_outputs)?;
1644 }
1645 Constraint::HorizontalDistance(distance) => {
1646 self.add_horizontal_distance(sketch, distance, &mut new_ast)
1647 .await
1648 .map_err(KclErrorWithOutputs::no_outputs)?;
1649 }
1650 Constraint::VerticalDistance(distance) => {
1651 self.add_vertical_distance(sketch, distance, &mut new_ast)
1652 .await
1653 .map_err(KclErrorWithOutputs::no_outputs)?;
1654 }
1655 Constraint::Horizontal(horizontal) => {
1656 self.add_horizontal(sketch, horizontal, &mut new_ast)
1657 .await
1658 .map_err(KclErrorWithOutputs::no_outputs)?;
1659 }
1660 Constraint::LinesEqualLength(lines_equal_length) => {
1661 self.add_lines_equal_length(sketch, lines_equal_length, &mut new_ast)
1662 .await
1663 .map_err(KclErrorWithOutputs::no_outputs)?;
1664 }
1665 Constraint::Midpoint(midpoint) => {
1666 self.add_midpoint(sketch, midpoint, &mut new_ast)
1667 .await
1668 .map_err(KclErrorWithOutputs::no_outputs)?;
1669 }
1670 Constraint::Parallel(parallel) => {
1671 self.add_parallel(sketch, parallel, &mut new_ast)
1672 .await
1673 .map_err(KclErrorWithOutputs::no_outputs)?;
1674 }
1675 Constraint::Perpendicular(perpendicular) => {
1676 self.add_perpendicular(sketch, perpendicular, &mut new_ast)
1677 .await
1678 .map_err(KclErrorWithOutputs::no_outputs)?;
1679 }
1680 Constraint::Vertical(vertical) => {
1681 self.add_vertical(sketch, vertical, &mut new_ast)
1682 .await
1683 .map_err(KclErrorWithOutputs::no_outputs)?;
1684 }
1685 Constraint::Diameter(diameter) => {
1686 self.add_diameter(sketch, diameter, &mut new_ast)
1687 .await
1688 .map_err(KclErrorWithOutputs::no_outputs)?;
1689 }
1690 Constraint::Radius(radius) => {
1691 self.add_radius(sketch, radius, &mut new_ast)
1692 .await
1693 .map_err(KclErrorWithOutputs::no_outputs)?;
1694 }
1695 Constraint::Symmetric(symmetric) => {
1696 self.add_symmetric(sketch, symmetric, &mut new_ast)
1697 .await
1698 .map_err(KclErrorWithOutputs::no_outputs)?;
1699 }
1700 Constraint::Angle(angle) => {
1701 self.add_angle(sketch, angle, &mut new_ast)
1702 .await
1703 .map_err(KclErrorWithOutputs::no_outputs)?;
1704 }
1705 Constraint::Tangent(tangent) => {
1706 self.add_tangent(sketch, tangent, &mut new_ast)
1707 .await
1708 .map_err(KclErrorWithOutputs::no_outputs)?;
1709 }
1710 }
1711 }
1712
1713 let constraint_ids_set = delete_constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
1715
1716 let has_constraint_deletions = !constraint_ids_set.is_empty();
1717 for constraint_id in constraint_ids_set {
1718 self.delete_constraint(&mut new_ast, sketch, constraint_id)
1719 .map_err(KclErrorWithOutputs::no_outputs)?;
1720 }
1721
1722 let (source_delta, mut scene_graph_delta) = self
1726 .execute_after_edit(
1727 ctx,
1728 sketch,
1729 sketch_block_ref,
1730 &mut new_ast,
1731 ExecuteAfterEditOptions {
1732 segment_ids_edited,
1733 edit_kind: EditDeleteKind::Edit,
1734 commit_solved_initial_guesses: true,
1735 },
1736 )
1737 .await?;
1738
1739 if has_constraint_deletions {
1742 scene_graph_delta.invalidates_ids = true;
1743 }
1744
1745 Ok((source_delta, scene_graph_delta))
1746 }
1747
1748 async fn batch_tail_cut_operations(
1749 &mut self,
1750 ctx: &ExecutorContext,
1751 _version: Version,
1752 sketch: ObjectId,
1753 edit_segments: Vec<ExistingSegmentCtor>,
1754 add_constraints: Vec<Constraint>,
1755 delete_constraint_ids: Vec<ObjectId>,
1756 additional_edited_segment_ids: Vec<ObjectId>,
1757 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1758 let sketch_block_ref =
1759 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1760
1761 let mut new_ast = self.program.ast.clone();
1762 let mut segment_ids_edited = AhashIndexSet::with_capacity_and_hasher(edit_segments.len(), Default::default());
1763
1764 for segment in edit_segments {
1766 segment_ids_edited.insert(segment.id);
1767 match segment.ctor {
1768 SegmentCtor::Point(ctor) => self
1769 .edit_point(&mut new_ast, sketch, segment.id, ctor)
1770 .map_err(KclErrorWithOutputs::no_outputs)?,
1771 SegmentCtor::Line(ctor) => self
1772 .edit_line(&mut new_ast, sketch, segment.id, ctor)
1773 .map_err(KclErrorWithOutputs::no_outputs)?,
1774 SegmentCtor::Arc(ctor) => self
1775 .edit_arc(&mut new_ast, sketch, segment.id, ctor)
1776 .map_err(KclErrorWithOutputs::no_outputs)?,
1777 SegmentCtor::Circle(ctor) => self
1778 .edit_circle(&mut new_ast, sketch, segment.id, ctor)
1779 .map_err(KclErrorWithOutputs::no_outputs)?,
1780 SegmentCtor::ControlPointSpline(ctor) => self
1781 .edit_control_point_spline(&mut new_ast, sketch, segment.id, ctor)
1782 .map_err(KclErrorWithOutputs::no_outputs)?,
1783 }
1784 }
1785
1786 segment_ids_edited.extend(additional_edited_segment_ids);
1787
1788 for constraint in add_constraints {
1790 match constraint {
1791 Constraint::Coincident(coincident) => {
1792 self.add_coincident(sketch, coincident, &mut new_ast)
1793 .await
1794 .map_err(KclErrorWithOutputs::no_outputs)?;
1795 }
1796 other => {
1797 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1798 "unsupported constraint in tail cut batch: {other:?}"
1799 ))));
1800 }
1801 }
1802 }
1803
1804 let constraint_ids_set = delete_constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
1806
1807 let has_constraint_deletions = !constraint_ids_set.is_empty();
1808 for constraint_id in constraint_ids_set {
1809 self.delete_constraint(&mut new_ast, sketch, constraint_id)
1810 .map_err(KclErrorWithOutputs::no_outputs)?;
1811 }
1812
1813 let (source_delta, mut scene_graph_delta) = self
1817 .execute_after_edit(
1818 ctx,
1819 sketch,
1820 sketch_block_ref,
1821 &mut new_ast,
1822 ExecuteAfterEditOptions {
1823 segment_ids_edited,
1824 edit_kind: EditDeleteKind::Edit,
1825 commit_solved_initial_guesses: true,
1826 },
1827 )
1828 .await?;
1829
1830 if has_constraint_deletions {
1833 scene_graph_delta.invalidates_ids = true;
1834 }
1835
1836 Ok((source_delta, scene_graph_delta))
1837 }
1838}
1839
1840impl FrontendState {
1841 pub async fn hack_set_program(&mut self, ctx: &ExecutorContext, program: Program) -> ExecResult<SetProgramOutcome> {
1842 self.program = program.clone();
1843
1844 self.point_freedom_cache.clear();
1855 match ctx.run_with_caching(program).await {
1856 Ok(outcome) => {
1857 let outcome = self.update_state_after_exec(outcome, true);
1858 let checkpoint_id = self
1859 .create_sketch_checkpoint(outcome.clone())
1860 .await
1861 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
1862 Ok(SetProgramOutcome::Success {
1863 scene_graph: Box::new(self.scene_graph_for_ui()),
1864 exec_outcome: Box::new(outcome),
1865 checkpoint_id: Some(checkpoint_id),
1866 })
1867 }
1868 Err(mut err) => {
1869 let outcome = self.exec_outcome_from_exec_error(err.clone())?;
1872 self.update_state_after_exec(outcome, true);
1873 err.scene_graph = Some(self.scene_graph_for_ui());
1874 Ok(SetProgramOutcome::ExecFailure { error: Box::new(err) })
1875 }
1876 }
1877 }
1878
1879 pub async fn engine_execute(
1882 &mut self,
1883 ctx: &ExecutorContext,
1884 program: Program,
1885 ) -> Result<SceneGraphDelta, KclErrorWithOutputs> {
1886 self.program = program.clone();
1887
1888 self.point_freedom_cache.clear();
1892 match ctx.run_with_caching(program).await {
1893 Ok(outcome) => {
1894 let outcome = self.update_state_after_exec(outcome, true);
1895 Ok(SceneGraphDelta {
1896 new_graph: self.scene_graph_for_ui(),
1897 exec_outcome: outcome,
1898 new_objects: Default::default(),
1900 invalidates_ids: Default::default(),
1902 })
1903 }
1904 Err(mut err) => {
1905 let outcome = self.exec_outcome_from_exec_error(err.clone())?;
1907 self.update_state_after_exec(outcome, true);
1908 err.scene_graph = Some(self.scene_graph_for_ui());
1909 Err(err)
1910 }
1911 }
1912 }
1913
1914 fn exec_outcome_from_exec_error(&self, err: KclErrorWithOutputs) -> Result<ExecOutcome, KclErrorWithOutputs> {
1915 if matches!(err.error, KclError::EngineHangup { .. }) {
1916 return Err(err);
1920 }
1921
1922 let KclErrorWithOutputs {
1923 error,
1924 mut non_fatal,
1925 variables,
1926 operations,
1927 artifact_graph,
1928 scene_objects,
1929 source_range_to_object,
1930 var_solutions,
1931 filenames,
1932 default_planes,
1933 ..
1934 } = err;
1935
1936 if let Some(source_range) = error.source_ranges().first() {
1937 non_fatal.push(CompilationIssue::fatal(*source_range, error.get_message()));
1938 } else {
1939 non_fatal.push(CompilationIssue::fatal(SourceRange::synthetic(), error.get_message()));
1940 }
1941
1942 Ok(ExecOutcome {
1943 variables,
1944 filenames,
1945 operations,
1946 artifact_graph,
1947 scene_objects,
1948 source_range_to_object,
1949 var_solutions,
1950 issues: non_fatal,
1951 default_planes,
1952 })
1953 }
1954
1955 async fn add_point(
1956 &mut self,
1957 ctx: &ExecutorContext,
1958 sketch: ObjectId,
1959 ctor: PointCtor,
1960 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1961 let at_ast = to_ast_point2d(&ctor.position)
1963 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1964 let point_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
1965 callee: ast::Node::no_src(ast_sketch2_name(POINT_FN)),
1966 unlabeled: None,
1967 arguments: vec![ast::LabeledArg {
1968 label: Some(ast::Identifier::new(POINT_AT_PARAM)),
1969 arg: at_ast,
1970 }],
1971 digest: None,
1972 non_code_meta: Default::default(),
1973 })));
1974
1975 let sketch_id = sketch;
1977 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
1978 #[cfg(target_arch = "wasm32")]
1979 web_sys::console::error_1(
1980 &format!(
1981 "Sketch not found; sketch_id={sketch_id:?}, self.scene_graph.objects={:#?}",
1982 &self.scene_graph.objects
1983 )
1984 .into(),
1985 );
1986 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
1987 })?;
1988 let ObjectKind::Sketch(_) = &sketch_object.kind else {
1989 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1990 "Object is not a sketch, it is {}",
1991 sketch_object.kind.human_friendly_kind_with_article(),
1992 ))));
1993 };
1994 let mut new_ast = self.program.ast.clone();
1996 let (sketch_block_ref, _) = self
1997 .mutate_ast(
1998 &mut new_ast,
1999 sketch_id,
2000 AstMutateCommand::AddSketchBlockExprStmt { expr: point_ast },
2001 )
2002 .map_err(KclErrorWithOutputs::no_outputs)?;
2003 let new_source = source_from_ast(&new_ast);
2005 let (new_program, errors) = Program::parse(&new_source)
2007 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2008 if !errors.is_empty() {
2009 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2010 "Error parsing KCL source after adding point: {errors:?}"
2011 ))));
2012 }
2013 let Some(new_program) = new_program else {
2014 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
2015 "No AST produced after adding point".to_string(),
2016 )));
2017 };
2018
2019 let point_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2020 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2021 "Source range of point not found in sketch block: {sketch_block_ref:?}; {err:?}"
2022 )))
2023 })?;
2024
2025 self.program = new_program.clone();
2027
2028 let mut truncated_program = new_program;
2030 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2031 .map_err(KclErrorWithOutputs::no_outputs)?;
2032
2033 let outcome = ctx
2035 .run_mock(
2036 &truncated_program,
2037 &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2038 )
2039 .await?;
2040
2041 let new_object_ids = {
2042 let make_err =
2043 |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2044 let segment_id = outcome
2045 .source_range_to_object
2046 .get(&point_node_ref.range)
2047 .copied()
2048 .ok_or_else(|| make_err(format!("Source range of point not found: {point_node_ref:?}")))?;
2049 let segment_object = outcome
2050 .scene_objects
2051 .get(segment_id.0)
2052 .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2053 let ObjectKind::Segment { segment } = &segment_object.kind else {
2054 return Err(make_err(format!(
2055 "Object is not a segment, it is {}",
2056 segment_object.kind.human_friendly_kind_with_article()
2057 )));
2058 };
2059 let Segment::Point(_) = segment else {
2060 return Err(make_err(format!(
2061 "Segment is not a point, it is {}",
2062 segment.human_friendly_kind_with_article()
2063 )));
2064 };
2065 vec![segment_id]
2066 };
2067 let src_delta = SourceDelta { text: new_source };
2068 let outcome = self.update_state_after_exec(outcome, false);
2070 let scene_graph_delta = SceneGraphDelta {
2071 new_graph: self.scene_graph_for_ui(),
2072 invalidates_ids: false,
2073 new_objects: new_object_ids,
2074 exec_outcome: outcome,
2075 };
2076 Ok((src_delta, scene_graph_delta))
2077 }
2078
2079 async fn add_line(
2080 &mut self,
2081 ctx: &ExecutorContext,
2082 sketch: ObjectId,
2083 ctor: LineCtor,
2084 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2085 let start_ast = to_ast_point2d(&ctor.start)
2087 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2088 let end_ast = to_ast_point2d(&ctor.end)
2089 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2090 let mut arguments = vec![
2091 ast::LabeledArg {
2092 label: Some(ast::Identifier::new(LINE_START_PARAM)),
2093 arg: start_ast,
2094 },
2095 ast::LabeledArg {
2096 label: Some(ast::Identifier::new(LINE_END_PARAM)),
2097 arg: end_ast,
2098 },
2099 ];
2100 if ctor.construction == Some(true) {
2102 arguments.push(ast::LabeledArg {
2103 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2104 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
2105 value: ast::LiteralValue::Bool(true),
2106 raw: "true".to_string(),
2107 digest: None,
2108 }))),
2109 });
2110 }
2111 let line_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
2112 callee: ast::Node::no_src(ast_sketch2_name(LINE_FN)),
2113 unlabeled: None,
2114 arguments,
2115 digest: None,
2116 non_code_meta: Default::default(),
2117 })));
2118
2119 let sketch_id = sketch;
2121 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2122 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2123 })?;
2124 let ObjectKind::Sketch(_) = &sketch_object.kind else {
2125 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2126 "Object is not a sketch, it is {}",
2127 sketch_object.kind.human_friendly_kind_with_article(),
2128 ))));
2129 };
2130 let mut new_ast = self.program.ast.clone();
2132 let (sketch_block_ref, _) = self
2133 .mutate_ast(
2134 &mut new_ast,
2135 sketch_id,
2136 AstMutateCommand::AddSketchBlockExprStmt { expr: line_ast },
2137 )
2138 .map_err(KclErrorWithOutputs::no_outputs)?;
2139 let new_source = source_from_ast(&new_ast);
2141 let (new_program, errors) = Program::parse(&new_source)
2143 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2144 if !errors.is_empty() {
2145 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2146 "Error parsing KCL source after adding line: {errors:?}"
2147 ))));
2148 }
2149 let Some(new_program) = new_program else {
2150 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
2151 "No AST produced after adding line".to_string(),
2152 )));
2153 };
2154
2155 let line_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2156 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2157 "Source range of line not found in sketch block: {sketch_block_ref:?}; {err:?}"
2158 )))
2159 })?;
2160
2161 self.program = new_program.clone();
2163
2164 let mut truncated_program = new_program;
2166 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2167 .map_err(KclErrorWithOutputs::no_outputs)?;
2168
2169 let outcome = ctx
2171 .run_mock(
2172 &truncated_program,
2173 &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2174 )
2175 .await?;
2176
2177 let new_object_ids = {
2178 let make_err =
2179 |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2180 let segment_id = outcome
2181 .source_range_to_object
2182 .get(&line_node_ref.range)
2183 .copied()
2184 .ok_or_else(|| make_err(format!("Source range of line not found: {line_node_ref:?}")))?;
2185 let segment_object = outcome
2186 .scene_object_by_id(segment_id)
2187 .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2188 let ObjectKind::Segment { segment } = &segment_object.kind else {
2189 return Err(make_err(format!(
2190 "Object is not a segment, it is {}",
2191 segment_object.kind.human_friendly_kind_with_article()
2192 )));
2193 };
2194 let Segment::Line(line) = segment else {
2195 return Err(make_err(format!(
2196 "Segment is not a line, it is {}",
2197 segment.human_friendly_kind_with_article()
2198 )));
2199 };
2200 vec![line.start, line.end, segment_id]
2201 };
2202 let src_delta = SourceDelta { text: new_source };
2203 let outcome = self.update_state_after_exec(outcome, false);
2205 let scene_graph_delta = SceneGraphDelta {
2206 new_graph: self.scene_graph_for_ui(),
2207 invalidates_ids: false,
2208 new_objects: new_object_ids,
2209 exec_outcome: outcome,
2210 };
2211 Ok((src_delta, scene_graph_delta))
2212 }
2213
2214 async fn add_arc(
2215 &mut self,
2216 ctx: &ExecutorContext,
2217 sketch: ObjectId,
2218 ctor: ArcCtor,
2219 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2220 let start_ast = to_ast_point2d(&ctor.start)
2222 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2223 let end_ast = to_ast_point2d(&ctor.end)
2224 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2225 let center_ast = to_ast_point2d(&ctor.center)
2226 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2227 let mut arguments = vec![
2228 ast::LabeledArg {
2229 label: Some(ast::Identifier::new(ARC_START_PARAM)),
2230 arg: start_ast,
2231 },
2232 ast::LabeledArg {
2233 label: Some(ast::Identifier::new(ARC_END_PARAM)),
2234 arg: end_ast,
2235 },
2236 ast::LabeledArg {
2237 label: Some(ast::Identifier::new(ARC_CENTER_PARAM)),
2238 arg: center_ast,
2239 },
2240 ];
2241 if ctor.construction == Some(true) {
2243 arguments.push(ast::LabeledArg {
2244 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2245 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
2246 value: ast::LiteralValue::Bool(true),
2247 raw: "true".to_string(),
2248 digest: None,
2249 }))),
2250 });
2251 }
2252 let arc_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
2253 callee: ast::Node::no_src(ast_sketch2_name(ARC_FN)),
2254 unlabeled: None,
2255 arguments,
2256 digest: None,
2257 non_code_meta: Default::default(),
2258 })));
2259
2260 let sketch_id = sketch;
2262 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2263 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2264 })?;
2265 let ObjectKind::Sketch(_) = &sketch_object.kind else {
2266 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2267 "Object is not a sketch, it is {}",
2268 sketch_object.kind.human_friendly_kind_with_article(),
2269 ))));
2270 };
2271 let mut new_ast = self.program.ast.clone();
2273 let (sketch_block_ref, _) = self
2274 .mutate_ast(
2275 &mut new_ast,
2276 sketch_id,
2277 AstMutateCommand::AddSketchBlockExprStmt { expr: arc_ast },
2278 )
2279 .map_err(KclErrorWithOutputs::no_outputs)?;
2280 let new_source = source_from_ast(&new_ast);
2282 let (new_program, errors) = Program::parse(&new_source)
2284 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2285 if !errors.is_empty() {
2286 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2287 "Error parsing KCL source after adding arc: {errors:?}"
2288 ))));
2289 }
2290 let Some(new_program) = new_program else {
2291 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
2292 "No AST produced after adding arc".to_string(),
2293 )));
2294 };
2295
2296 let arc_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2297 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2298 "Source range of arc not found in sketch block: {sketch_block_ref:?}; {err:?}"
2299 )))
2300 })?;
2301
2302 self.program = new_program.clone();
2304
2305 let mut truncated_program = new_program;
2307 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2308 .map_err(KclErrorWithOutputs::no_outputs)?;
2309
2310 let outcome = ctx
2312 .run_mock(
2313 &truncated_program,
2314 &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2315 )
2316 .await?;
2317
2318 let new_object_ids = {
2319 let make_err =
2320 |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2321 let segment_id = outcome
2322 .source_range_to_object
2323 .get(&arc_node_ref.range)
2324 .copied()
2325 .ok_or_else(|| make_err(format!("Source range of arc not found: {arc_node_ref:?}")))?;
2326 let segment_object = outcome
2327 .scene_objects
2328 .get(segment_id.0)
2329 .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2330 let ObjectKind::Segment { segment } = &segment_object.kind else {
2331 return Err(make_err(format!(
2332 "Object is not a segment, it is {}",
2333 segment_object.kind.human_friendly_kind_with_article()
2334 )));
2335 };
2336 let Segment::Arc(arc) = segment else {
2337 return Err(make_err(format!(
2338 "Segment is not an arc, it is {}",
2339 segment.human_friendly_kind_with_article()
2340 )));
2341 };
2342 vec![arc.start, arc.end, arc.center, segment_id]
2343 };
2344 let src_delta = SourceDelta { text: new_source };
2345 let outcome = self.update_state_after_exec(outcome, false);
2347 let scene_graph_delta = SceneGraphDelta {
2348 new_graph: self.scene_graph_for_ui(),
2349 invalidates_ids: false,
2350 new_objects: new_object_ids,
2351 exec_outcome: outcome,
2352 };
2353 Ok((src_delta, scene_graph_delta))
2354 }
2355
2356 async fn add_circle(
2357 &mut self,
2358 ctx: &ExecutorContext,
2359 sketch: ObjectId,
2360 ctor: CircleCtor,
2361 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2362 let start_ast = to_ast_point2d(&ctor.start)
2364 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2365 let center_ast = to_ast_point2d(&ctor.center)
2366 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2367 let mut arguments = vec![
2368 ast::LabeledArg {
2369 label: Some(ast::Identifier::new(CIRCLE_START_PARAM)),
2370 arg: start_ast,
2371 },
2372 ast::LabeledArg {
2373 label: Some(ast::Identifier::new(CIRCLE_CENTER_PARAM)),
2374 arg: center_ast,
2375 },
2376 ];
2377 if ctor.construction == Some(true) {
2379 arguments.push(ast::LabeledArg {
2380 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2381 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
2382 value: ast::LiteralValue::Bool(true),
2383 raw: "true".to_string(),
2384 digest: None,
2385 }))),
2386 });
2387 }
2388 let circle_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
2389 callee: ast::Node::no_src(ast_sketch2_name(CIRCLE_FN)),
2390 unlabeled: None,
2391 arguments,
2392 digest: None,
2393 non_code_meta: Default::default(),
2394 })));
2395
2396 let sketch_id = sketch;
2398 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2399 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2400 })?;
2401 let ObjectKind::Sketch(_) = &sketch_object.kind else {
2402 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2403 "Object is not a sketch, it is {}",
2404 sketch_object.kind.human_friendly_kind_with_article(),
2405 ))));
2406 };
2407 let mut new_ast = self.program.ast.clone();
2409 let (sketch_block_ref, _) = self
2410 .mutate_ast(
2411 &mut new_ast,
2412 sketch_id,
2413 AstMutateCommand::AddSketchBlockVarDecl {
2414 prefix: CIRCLE_VARIABLE.to_owned(),
2415 expr: circle_ast,
2416 },
2417 )
2418 .map_err(KclErrorWithOutputs::no_outputs)?;
2419 let new_source = source_from_ast(&new_ast);
2421 let (new_program, errors) = Program::parse(&new_source)
2423 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2424 if !errors.is_empty() {
2425 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2426 "Error parsing KCL source after adding circle: {errors:?}"
2427 ))));
2428 }
2429 let Some(new_program) = new_program else {
2430 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
2431 "No AST produced after adding circle".to_string(),
2432 )));
2433 };
2434
2435 let circle_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2436 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2437 "Source range of circle not found in sketch block: {sketch_block_ref:?}; {err:?}"
2438 )))
2439 })?;
2440
2441 self.program = new_program.clone();
2443
2444 let mut truncated_program = new_program;
2446 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2447 .map_err(KclErrorWithOutputs::no_outputs)?;
2448
2449 let outcome = ctx
2451 .run_mock(
2452 &truncated_program,
2453 &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2454 )
2455 .await?;
2456
2457 let new_object_ids = {
2458 let make_err =
2459 |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2460 let segment_id = outcome
2461 .source_range_to_object
2462 .get(&circle_node_ref.range)
2463 .copied()
2464 .ok_or_else(|| make_err(format!("Source range of circle not found: {circle_node_ref:?}")))?;
2465 let segment_object = outcome
2466 .scene_objects
2467 .get(segment_id.0)
2468 .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2469 let ObjectKind::Segment { segment } = &segment_object.kind else {
2470 return Err(make_err(format!(
2471 "Object is not a segment, it is {}",
2472 segment_object.kind.human_friendly_kind_with_article()
2473 )));
2474 };
2475 let Segment::Circle(circle) = segment else {
2476 return Err(make_err(format!(
2477 "Segment is not a circle, it is {}",
2478 segment.human_friendly_kind_with_article()
2479 )));
2480 };
2481 vec![circle.start, circle.center, segment_id]
2482 };
2483 let src_delta = SourceDelta { text: new_source };
2484 let outcome = self.update_state_after_exec(outcome, false);
2486 let scene_graph_delta = SceneGraphDelta {
2487 new_graph: self.scene_graph_for_ui(),
2488 invalidates_ids: false,
2489 new_objects: new_object_ids,
2490 exec_outcome: outcome,
2491 };
2492 Ok((src_delta, scene_graph_delta))
2493 }
2494
2495 async fn add_control_point_spline(
2496 &mut self,
2497 ctx: &ExecutorContext,
2498 sketch: ObjectId,
2499 ctor: ControlPointSplineCtor,
2500 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2501 let new_program = ensure_control_point_spline_experimental_features(&self.program)
2502 .map_err(KclErrorWithOutputs::no_outputs)?;
2503
2504 let points_ast = to_ast_point2d_array(&ctor.points)
2505 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2506 let mut arguments = vec![ast::LabeledArg {
2507 label: Some(ast::Identifier::new(CONTROL_POINT_SPLINE_POINTS_PARAM)),
2508 arg: points_ast,
2509 }];
2510 if ctor.construction == Some(true) {
2511 arguments.push(ast::LabeledArg {
2512 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2513 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
2514 value: ast::LiteralValue::Bool(true),
2515 raw: "true".to_string(),
2516 digest: None,
2517 }))),
2518 });
2519 }
2520 let spline_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
2521 callee: ast::Node::no_src(ast_sketch2_name(CONTROL_POINT_SPLINE_FN)),
2522 unlabeled: None,
2523 arguments,
2524 digest: None,
2525 non_code_meta: Default::default(),
2526 })));
2527
2528 let sketch_object = self.scene_graph.objects.get(sketch.0).ok_or_else(|| {
2529 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2530 })?;
2531 let ObjectKind::Sketch(_) = &sketch_object.kind else {
2532 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2533 "Object is not a sketch, it is {}",
2534 sketch_object.kind.human_friendly_kind_with_article(),
2535 ))));
2536 };
2537
2538 let mut new_ast = new_program.ast.clone();
2539 let (sketch_block_ref, _) = self
2540 .mutate_ast(
2541 &mut new_ast,
2542 sketch,
2543 AstMutateCommand::AddSketchBlockExprStmt { expr: spline_ast },
2544 )
2545 .map_err(KclErrorWithOutputs::no_outputs)?;
2546 let new_source = source_from_ast(&new_ast);
2547 let (new_program, errors) = Program::parse(&new_source)
2548 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2549 if !errors.is_empty() {
2550 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2551 "Error parsing KCL source after adding controlPointSpline: {errors:?}"
2552 ))));
2553 }
2554 let Some(new_program) = new_program else {
2555 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
2556 "No AST produced after adding controlPointSpline".to_string(),
2557 )));
2558 };
2559
2560 let spline_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2561 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2562 "Source range of controlPointSpline not found in sketch block: {sketch_block_ref:?}; {err:?}"
2563 )))
2564 })?;
2565
2566 self.program = new_program.clone();
2567
2568 let mut truncated_program = new_program;
2569 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2570 .map_err(KclErrorWithOutputs::no_outputs)?;
2571
2572 let outcome = ctx
2573 .run_mock(
2574 &truncated_program,
2575 &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2576 )
2577 .await?;
2578
2579 let new_object_ids = {
2580 let make_err =
2581 |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2582 let segment_id = outcome
2583 .source_range_to_object
2584 .get(&spline_node_ref.range)
2585 .copied()
2586 .ok_or_else(|| {
2587 make_err(format!(
2588 "Source range of controlPointSpline not found: {spline_node_ref:?}"
2589 ))
2590 })?;
2591 let segment_object = outcome
2592 .scene_objects
2593 .get(segment_id.0)
2594 .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2595 let ObjectKind::Segment { segment } = &segment_object.kind else {
2596 return Err(make_err(format!(
2597 "Object is not a segment, it is {}",
2598 segment_object.kind.human_friendly_kind_with_article()
2599 )));
2600 };
2601 let Segment::ControlPointSpline(spline) = segment else {
2602 return Err(make_err(format!(
2603 "Segment is not a control point spline, it is {}",
2604 segment.human_friendly_kind_with_article()
2605 )));
2606 };
2607
2608 let mut ids = outcome
2609 .scene_objects
2610 .iter()
2611 .filter_map(|obj| match &obj.kind {
2612 ObjectKind::Segment {
2613 segment: Segment::Line(line),
2614 } if line.owner == Some(segment_id) => Some(obj.id),
2615 _ => None,
2616 })
2617 .collect::<Vec<_>>();
2618 ids.extend(spline.controls.clone());
2619 ids.push(segment_id);
2620 ids
2621 };
2622 let src_delta = SourceDelta { text: new_source };
2623 let outcome = self.update_state_after_exec(outcome, false);
2624 let scene_graph_delta = SceneGraphDelta {
2625 new_graph: self.scene_graph_for_ui(),
2626 invalidates_ids: false,
2627 new_objects: new_object_ids,
2628 exec_outcome: outcome,
2629 };
2630 Ok((src_delta, scene_graph_delta))
2631 }
2632
2633 fn edit_point(
2634 &mut self,
2635 new_ast: &mut ast::Node<ast::Program>,
2636 sketch: ObjectId,
2637 point: ObjectId,
2638 ctor: PointCtor,
2639 ) -> Result<(), KclError> {
2640 let new_at_ast = to_ast_point2d(&ctor.position).map_err(|err| KclError::refactor(err.to_string()))?;
2642
2643 let sketch_id = sketch;
2645 let sketch_object = self
2646 .scene_graph
2647 .objects
2648 .get(sketch_id.0)
2649 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2650 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2651 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2652 };
2653 sketch.segments.iter().find(|o| **o == point).ok_or_else(|| {
2654 KclError::refactor(format!("Point not found in sketch: point={point:?}, sketch={sketch:?}"))
2655 })?;
2656 let point_id = point;
2658 let point_object = self
2659 .scene_graph
2660 .objects
2661 .get(point_id.0)
2662 .ok_or_else(|| KclError::refactor(format!("Point not found in scene graph: point={point:?}")))?;
2663 let ObjectKind::Segment {
2664 segment: Segment::Point(point),
2665 } = &point_object.kind
2666 else {
2667 return Err(KclError::refactor(format!(
2668 "Object is not a point segment: {point_object:?}"
2669 )));
2670 };
2671
2672 if let Some(owner_id) = point.owner {
2674 let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
2675 KclError::refactor(format!(
2676 "Internal: Owner of point not found in scene graph: owner={owner_id:?}",
2677 ))
2678 })?;
2679 let ObjectKind::Segment { segment } = &owner_object.kind else {
2680 return Err(KclError::refactor(format!(
2681 "Internal: Owner of point is not a segment, but found {}",
2682 owner_object.kind.human_friendly_kind_with_article()
2683 )));
2684 };
2685
2686 if let Segment::Line(line) = segment {
2688 let SegmentCtor::Line(line_ctor) = &line.ctor else {
2689 return Err(KclError::refactor(format!(
2690 "Internal: Owner of point does not have line ctor, but found {}",
2691 line.ctor.human_friendly_kind_with_article()
2692 )));
2693 };
2694 let mut line_ctor = line_ctor.clone();
2695 if line.start == point_id {
2697 line_ctor.start = ctor.position;
2698 } else if line.end == point_id {
2699 line_ctor.end = ctor.position;
2700 } else {
2701 return Err(KclError::refactor(format!(
2702 "Internal: Point is not part of owner's line segment: point={point_id:?}, line={owner_id:?}"
2703 )));
2704 }
2705 return self.edit_line(new_ast, sketch_id, owner_id, line_ctor);
2706 }
2707
2708 if let Segment::Arc(arc) = segment {
2710 let SegmentCtor::Arc(arc_ctor) = &arc.ctor else {
2711 return Err(KclError::refactor(format!(
2712 "Internal: Owner of point does not have arc ctor, but found {}",
2713 arc.ctor.human_friendly_kind_with_article()
2714 )));
2715 };
2716 let mut arc_ctor = arc_ctor.clone();
2717 if arc.center == point_id {
2719 arc_ctor.center = ctor.position;
2720 } else if arc.start == point_id {
2721 arc_ctor.start = ctor.position;
2722 } else if arc.end == point_id {
2723 arc_ctor.end = ctor.position;
2724 } else {
2725 return Err(KclError::refactor(format!(
2726 "Internal: Point is not part of owner's arc segment: point={point_id:?}, arc={owner_id:?}"
2727 )));
2728 }
2729 return self.edit_arc(new_ast, sketch_id, owner_id, arc_ctor);
2730 }
2731
2732 if let Segment::Circle(circle) = segment {
2734 let SegmentCtor::Circle(circle_ctor) = &circle.ctor else {
2735 return Err(KclError::refactor(format!(
2736 "Internal: Owner of point does not have circle ctor, but found {}",
2737 circle.ctor.human_friendly_kind_with_article()
2738 )));
2739 };
2740 let mut circle_ctor = circle_ctor.clone();
2741 if circle.center == point_id {
2742 circle_ctor.center = ctor.position;
2743 } else if circle.start == point_id {
2744 circle_ctor.start = ctor.position;
2745 } else {
2746 return Err(KclError::refactor(format!(
2747 "Internal: Point is not part of owner's circle segment: point={point_id:?}, circle={owner_id:?}"
2748 )));
2749 }
2750 return self.edit_circle(new_ast, sketch_id, owner_id, circle_ctor);
2751 }
2752
2753 if let Segment::ControlPointSpline(spline) = segment {
2754 let SegmentCtor::ControlPointSpline(spline_ctor) = &spline.ctor else {
2755 return Err(KclError::refactor(format!(
2756 "Internal: Owner of point does not have controlPointSpline ctor, but found {}",
2757 spline.ctor.human_friendly_kind_with_article()
2758 )));
2759 };
2760 let mut spline_ctor = spline_ctor.clone();
2761 let Some(control_index) = spline.controls.iter().position(|id| *id == point_id) else {
2762 return Err(KclError::refactor(format!(
2763 "Internal: Point is not part of owner's controlPointSpline segment: point={point_id:?}, spline={owner_id:?}"
2764 )));
2765 };
2766 spline_ctor.points[control_index] = ctor.position;
2767 return self.edit_control_point_spline(new_ast, sketch_id, owner_id, spline_ctor);
2768 }
2769
2770 }
2773
2774 self.mutate_ast(new_ast, point_id, AstMutateCommand::EditPoint { at: new_at_ast })?;
2776 Ok(())
2777 }
2778
2779 fn edit_line(
2780 &mut self,
2781 new_ast: &mut ast::Node<ast::Program>,
2782 sketch: ObjectId,
2783 line: ObjectId,
2784 ctor: LineCtor,
2785 ) -> Result<(), KclError> {
2786 let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2788 let new_end_ast = to_ast_point2d(&ctor.end).map_err(|err| KclError::refactor(err.to_string()))?;
2789
2790 let sketch_id = sketch;
2792 let sketch_object = self
2793 .scene_graph
2794 .objects
2795 .get(sketch_id.0)
2796 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2797 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2798 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2799 };
2800 sketch
2801 .segments
2802 .iter()
2803 .find(|o| **o == line)
2804 .ok_or_else(|| KclError::refactor(format!("Line not found in sketch: line={line:?}, sketch={sketch:?}")))?;
2805 let line_id = line;
2807 let line_object = self
2808 .scene_graph
2809 .objects
2810 .get(line_id.0)
2811 .ok_or_else(|| KclError::refactor(format!("Line not found in scene graph: line={line:?}")))?;
2812 let ObjectKind::Segment { .. } = &line_object.kind else {
2813 let kind = line_object.kind.human_friendly_kind_with_article();
2814 return Err(KclError::refactor(format!(
2815 "This constraint only works on Segments, but you selected {kind}"
2816 )));
2817 };
2818
2819 self.mutate_ast(
2821 new_ast,
2822 line_id,
2823 AstMutateCommand::EditLine {
2824 start: new_start_ast,
2825 end: new_end_ast,
2826 construction: ctor.construction,
2827 },
2828 )?;
2829 Ok(())
2830 }
2831
2832 fn edit_arc(
2833 &mut self,
2834 new_ast: &mut ast::Node<ast::Program>,
2835 sketch: ObjectId,
2836 arc: ObjectId,
2837 ctor: ArcCtor,
2838 ) -> Result<(), KclError> {
2839 let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2841 let new_end_ast = to_ast_point2d(&ctor.end).map_err(|err| KclError::refactor(err.to_string()))?;
2842 let new_center_ast = to_ast_point2d(&ctor.center).map_err(|err| KclError::refactor(err.to_string()))?;
2843
2844 let sketch_id = sketch;
2846 let sketch_object = self
2847 .scene_graph
2848 .objects
2849 .get(sketch_id.0)
2850 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2851 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2852 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2853 };
2854 sketch
2855 .segments
2856 .iter()
2857 .find(|o| **o == arc)
2858 .ok_or_else(|| KclError::refactor(format!("Arc not found in sketch: arc={arc:?}, sketch={sketch:?}")))?;
2859 let arc_id = arc;
2861 let arc_object = self
2862 .scene_graph
2863 .objects
2864 .get(arc_id.0)
2865 .ok_or_else(|| KclError::refactor(format!("Arc not found in scene graph: arc={arc:?}")))?;
2866 let ObjectKind::Segment { .. } = &arc_object.kind else {
2867 return Err(KclError::refactor(format!("Object is not a segment: {arc_object:?}")));
2868 };
2869
2870 self.mutate_ast(
2872 new_ast,
2873 arc_id,
2874 AstMutateCommand::EditArc {
2875 start: new_start_ast,
2876 end: new_end_ast,
2877 center: new_center_ast,
2878 construction: ctor.construction,
2879 },
2880 )?;
2881 Ok(())
2882 }
2883
2884 fn edit_circle(
2885 &mut self,
2886 new_ast: &mut ast::Node<ast::Program>,
2887 sketch: ObjectId,
2888 circle: ObjectId,
2889 ctor: CircleCtor,
2890 ) -> Result<(), KclError> {
2891 let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2893 let new_center_ast = to_ast_point2d(&ctor.center).map_err(|err| KclError::refactor(err.to_string()))?;
2894
2895 let sketch_id = sketch;
2897 let sketch_object = self
2898 .scene_graph
2899 .objects
2900 .get(sketch_id.0)
2901 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2902 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2903 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2904 };
2905 sketch.segments.iter().find(|o| **o == circle).ok_or_else(|| {
2906 KclError::refactor(format!(
2907 "Circle not found in sketch: circle={circle:?}, sketch={sketch:?}"
2908 ))
2909 })?;
2910 let circle_id = circle;
2912 let circle_object = self
2913 .scene_graph
2914 .objects
2915 .get(circle_id.0)
2916 .ok_or_else(|| KclError::refactor(format!("Circle not found in scene graph: circle={circle:?}")))?;
2917 let ObjectKind::Segment { .. } = &circle_object.kind else {
2918 return Err(KclError::refactor(format!(
2919 "Object is not a segment: {circle_object:?}"
2920 )));
2921 };
2922
2923 self.mutate_ast(
2925 new_ast,
2926 circle_id,
2927 AstMutateCommand::EditCircle {
2928 start: new_start_ast,
2929 center: new_center_ast,
2930 construction: ctor.construction,
2931 },
2932 )?;
2933 Ok(())
2934 }
2935
2936 fn edit_control_point_spline(
2937 &mut self,
2938 new_ast: &mut ast::Node<ast::Program>,
2939 sketch: ObjectId,
2940 spline: ObjectId,
2941 ctor: ControlPointSplineCtor,
2942 ) -> Result<(), KclError> {
2943 let points_ast = to_ast_point2d_array(&ctor.points).map_err(|err| KclError::refactor(err.to_string()))?;
2944
2945 let sketch_object = self
2946 .scene_graph
2947 .objects
2948 .get(sketch.0)
2949 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2950 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2951 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2952 };
2953 sketch.segments.iter().find(|o| **o == spline).ok_or_else(|| {
2954 KclError::refactor(format!(
2955 "Control point spline not found in sketch: spline={spline:?}, sketch={sketch:?}"
2956 ))
2957 })?;
2958
2959 let spline_object =
2960 self.scene_graph.objects.get(spline.0).ok_or_else(|| {
2961 KclError::refactor(format!("Control point spline not found in scene graph: {spline:?}"))
2962 })?;
2963 let ObjectKind::Segment { .. } = &spline_object.kind else {
2964 return Err(KclError::refactor(format!(
2965 "Object is not a segment: {spline_object:?}"
2966 )));
2967 };
2968
2969 self.mutate_ast(
2970 new_ast,
2971 spline,
2972 AstMutateCommand::EditControlPointSpline {
2973 points: points_ast,
2974 construction: ctor.construction,
2975 },
2976 )?;
2977 Ok(())
2978 }
2979
2980 fn delete_segment(
2981 &mut self,
2982 new_ast: &mut ast::Node<ast::Program>,
2983 sketch: ObjectId,
2984 segment_id: ObjectId,
2985 ) -> Result<(), KclError> {
2986 let sketch_id = sketch;
2988 let sketch_object = self
2989 .scene_graph
2990 .objects
2991 .get(sketch_id.0)
2992 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2993 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2994 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2995 };
2996 sketch.segments.iter().find(|o| **o == segment_id).ok_or_else(|| {
2997 KclError::refactor(format!(
2998 "Segment not found in sketch: segment={segment_id:?}, sketch={sketch:?}"
2999 ))
3000 })?;
3001 let segment_object =
3003 self.scene_graph.objects.get(segment_id.0).ok_or_else(|| {
3004 KclError::refactor(format!("Segment not found in scene graph: segment={segment_id:?}"))
3005 })?;
3006 let ObjectKind::Segment { .. } = &segment_object.kind else {
3007 return Err(KclError::refactor(format!(
3008 "Object is not a segment, it is {}",
3009 segment_object.kind.human_friendly_kind_with_article()
3010 )));
3011 };
3012
3013 self.mutate_ast(new_ast, segment_id, AstMutateCommand::DeleteNode)?;
3015 Ok(())
3016 }
3017
3018 fn delete_constraint(
3019 &mut self,
3020 new_ast: &mut ast::Node<ast::Program>,
3021 sketch: ObjectId,
3022 constraint_id: ObjectId,
3023 ) -> Result<(), KclError> {
3024 let sketch_id = sketch;
3026 let sketch_object = self
3027 .scene_graph
3028 .objects
3029 .get(sketch_id.0)
3030 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
3031 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
3032 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
3033 };
3034 sketch
3035 .constraints
3036 .iter()
3037 .find(|o| **o == constraint_id)
3038 .ok_or_else(|| {
3039 KclError::refactor(format!(
3040 "Constraint not found in sketch: constraint={constraint_id:?}, sketch={sketch:?}"
3041 ))
3042 })?;
3043 let constraint_object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
3045 KclError::refactor(format!(
3046 "Constraint not found in scene graph: constraint={constraint_id:?}"
3047 ))
3048 })?;
3049 let ObjectKind::Constraint { .. } = &constraint_object.kind else {
3050 return Err(KclError::refactor(format!(
3051 "Object is not a constraint, it is {}",
3052 constraint_object.kind.human_friendly_kind_with_article()
3053 )));
3054 };
3055
3056 self.mutate_ast(new_ast, constraint_id, AstMutateCommand::DeleteNode)?;
3058 Ok(())
3059 }
3060
3061 fn edit_coincident_constraint(
3062 &mut self,
3063 new_ast: &mut ast::Node<ast::Program>,
3064 constraint_id: ObjectId,
3065 segments: Vec<ConstraintSegment>,
3066 ) -> Result<(), KclError> {
3067 if segments.len() < 2 {
3068 return Err(KclError::refactor(format!(
3069 "Coincident constraint must have at least 2 inputs, got {}",
3070 segments.len()
3071 )));
3072 }
3073
3074 let segment_asts = segments
3075 .iter()
3076 .map(|segment| self.coincident_segment_to_ast(segment, new_ast))
3077 .collect::<Result<Vec<_>, _>>()?;
3078
3079 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
3080 elements: segment_asts,
3081 digest: None,
3082 non_code_meta: Default::default(),
3083 })));
3084
3085 self.mutate_ast(
3086 new_ast,
3087 constraint_id,
3088 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3089 )?;
3090 Ok(())
3091 }
3092
3093 fn edit_horizontal_points_constraint(
3094 &mut self,
3095 new_ast: &mut ast::Node<ast::Program>,
3096 constraint_id: ObjectId,
3097 points: Vec<ConstraintSegment>,
3098 ) -> Result<(), KclError> {
3099 self.edit_axis_points_constraint(new_ast, constraint_id, points, "Horizontal")
3100 }
3101
3102 fn edit_vertical_points_constraint(
3103 &mut self,
3104 new_ast: &mut ast::Node<ast::Program>,
3105 constraint_id: ObjectId,
3106 points: Vec<ConstraintSegment>,
3107 ) -> Result<(), KclError> {
3108 self.edit_axis_points_constraint(new_ast, constraint_id, points, "Vertical")
3109 }
3110
3111 fn edit_axis_points_constraint(
3112 &mut self,
3113 new_ast: &mut ast::Node<ast::Program>,
3114 constraint_id: ObjectId,
3115 points: Vec<ConstraintSegment>,
3116 constraint_name: &str,
3117 ) -> Result<(), KclError> {
3118 if points.len() < 2 {
3119 return Err(KclError::refactor(format!(
3120 "{constraint_name} points constraint must have at least 2 points, got {}",
3121 points.len()
3122 )));
3123 }
3124
3125 let point_asts = points
3126 .iter()
3127 .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
3128 .collect::<Result<Vec<_>, _>>()?;
3129
3130 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
3131 elements: point_asts,
3132 digest: None,
3133 non_code_meta: Default::default(),
3134 })));
3135
3136 self.mutate_ast(
3137 new_ast,
3138 constraint_id,
3139 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3140 )?;
3141 Ok(())
3142 }
3143
3144 fn edit_equal_length_constraint(
3146 &mut self,
3147 new_ast: &mut ast::Node<ast::Program>,
3148 constraint_id: ObjectId,
3149 lines: Vec<ObjectId>,
3150 ) -> Result<(), KclError> {
3151 if lines.len() < 2 {
3152 return Err(KclError::refactor(format!(
3153 "Lines equal length constraint must have at least 2 lines, got {}",
3154 lines.len()
3155 )));
3156 }
3157
3158 let line_asts = lines
3159 .iter()
3160 .map(|line_id| {
3161 let line_object = self
3162 .scene_graph
3163 .objects
3164 .get(line_id.0)
3165 .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3166 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3167 let kind = line_object.kind.human_friendly_kind_with_article();
3168 return Err(KclError::refactor(format!(
3169 "This constraint only works on Segments, but you selected {kind}"
3170 )));
3171 };
3172 let Segment::Line(_) = line_segment else {
3173 let kind = line_segment.human_friendly_kind_with_article();
3174 return Err(KclError::refactor(format!(
3175 "Only lines can be made equal length, but you selected {kind}"
3176 )));
3177 };
3178
3179 get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)
3180 })
3181 .collect::<Result<Vec<_>, _>>()?;
3182
3183 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
3184 elements: line_asts,
3185 digest: None,
3186 non_code_meta: Default::default(),
3187 })));
3188
3189 self.mutate_ast(
3190 new_ast,
3191 constraint_id,
3192 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3193 )?;
3194 Ok(())
3195 }
3196
3197 fn edit_parallel_constraint(
3199 &mut self,
3200 new_ast: &mut ast::Node<ast::Program>,
3201 constraint_id: ObjectId,
3202 lines: Vec<ObjectId>,
3203 ) -> Result<(), KclError> {
3204 if lines.len() < 2 {
3205 return Err(KclError::refactor(format!(
3206 "Parallel constraint must have at least 2 lines, got {}",
3207 lines.len()
3208 )));
3209 }
3210
3211 let line_asts = lines
3212 .iter()
3213 .map(|line_id| {
3214 let line_object = self
3215 .scene_graph
3216 .objects
3217 .get(line_id.0)
3218 .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3219 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3220 let kind = line_object.kind.human_friendly_kind_with_article();
3221 return Err(KclError::refactor(format!(
3222 "This constraint only works on Segments, but you selected {kind}"
3223 )));
3224 };
3225 let Segment::Line(_) = line_segment else {
3226 let kind = line_segment.human_friendly_kind_with_article();
3227 return Err(KclError::refactor(format!(
3228 "Only lines can be made parallel, but you selected {kind}"
3229 )));
3230 };
3231
3232 get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)
3233 })
3234 .collect::<Result<Vec<_>, _>>()?;
3235
3236 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
3237 elements: line_asts,
3238 digest: None,
3239 non_code_meta: Default::default(),
3240 })));
3241
3242 self.mutate_ast(
3243 new_ast,
3244 constraint_id,
3245 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3246 )?;
3247 Ok(())
3248 }
3249
3250 fn edit_equal_radius_constraint(
3252 &mut self,
3253 new_ast: &mut ast::Node<ast::Program>,
3254 constraint_id: ObjectId,
3255 input: Vec<ObjectId>,
3256 ) -> Result<(), KclError> {
3257 if input.len() < 2 {
3258 return Err(KclError::refactor(format!(
3259 "equalRadius constraint must have at least 2 segments, got {}",
3260 input.len()
3261 )));
3262 }
3263
3264 let input_asts = input
3265 .iter()
3266 .map(|segment_id| self.equal_radius_segment_id_to_ast_reference(*segment_id, new_ast))
3267 .collect::<Result<Vec<_>, _>>()?;
3268
3269 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
3270 elements: input_asts,
3271 digest: None,
3272 non_code_meta: Default::default(),
3273 })));
3274
3275 self.mutate_ast(
3276 new_ast,
3277 constraint_id,
3278 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3279 )?;
3280 Ok(())
3281 }
3282
3283 async fn execute_after_edit(
3284 &mut self,
3285 ctx: &ExecutorContext,
3286 sketch: ObjectId,
3287 sketch_block_ref: AstNodeRef,
3288 new_ast: &mut ast::Node<ast::Program>,
3289 options: ExecuteAfterEditOptions,
3290 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
3291 let ExecuteAfterEditOptions {
3292 segment_ids_edited,
3293 edit_kind,
3294 commit_solved_initial_guesses,
3295 } = options;
3296
3297 let new_source = source_from_ast(new_ast);
3299 let (new_program, errors) = Program::parse(&new_source)
3301 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
3302 if !errors.is_empty() {
3303 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
3304 "Error parsing KCL source after editing: {errors:?}"
3305 ))));
3306 }
3307 let Some(new_program) = new_program else {
3308 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
3309 "No AST produced after editing".to_string(),
3310 )));
3311 };
3312
3313 self.program = new_program.clone();
3315
3316 let is_delete = edit_kind.is_delete();
3318 let truncated_program = {
3319 let mut truncated_program = new_program;
3320 only_sketch_block(
3321 &mut truncated_program.ast,
3322 &sketch_block_ref,
3323 edit_kind.to_change_kind(),
3324 )
3325 .map_err(KclErrorWithOutputs::no_outputs)?;
3326 truncated_program
3327 };
3328
3329 let drag_anchors = self.next_segment_drag_anchors.take().unwrap_or_default();
3331 let mock_config = MockConfig {
3332 sketch_block_id: Some(sketch),
3333 freedom_analysis: is_delete,
3334 segment_ids_edited: segment_ids_edited.clone(),
3335 drag_anchors,
3336 ..Default::default()
3337 };
3338 let outcome = ctx.run_mock(&truncated_program, &mock_config).await?;
3339
3340 let outcome = self.update_state_after_exec(outcome, is_delete);
3342
3343 let src_delta = if commit_solved_initial_guesses {
3344 self.commit_var_solutions_to_program(&outcome, "editing")?
3345 } else {
3346 SourceDelta { text: new_source }
3347 };
3348 let scene_graph_delta = SceneGraphDelta {
3349 new_graph: self.scene_graph_for_ui(),
3350 invalidates_ids: is_delete,
3351 new_objects: Vec::new(),
3352 exec_outcome: outcome,
3353 };
3354 Ok((src_delta, scene_graph_delta))
3355 }
3356
3357 async fn execute_after_delete_sketch(
3358 &mut self,
3359 ctx: &ExecutorContext,
3360 new_ast: &mut ast::Node<ast::Program>,
3361 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
3362 let new_source = source_from_ast(new_ast);
3364 let (new_program, errors) = Program::parse(&new_source)
3366 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
3367 if !errors.is_empty() {
3368 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
3369 "Error parsing KCL source after editing: {errors:?}"
3370 ))));
3371 }
3372 let Some(new_program) = new_program else {
3373 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
3374 "No AST produced after editing".to_string(),
3375 )));
3376 };
3377
3378 self.program = new_program.clone();
3380
3381 let outcome = ctx.run_with_caching(new_program).await?;
3387 let freedom_analysis_ran = true;
3388
3389 let outcome = self.update_state_after_exec(outcome, freedom_analysis_ran);
3390
3391 let src_delta = SourceDelta { text: new_source };
3392 let scene_graph_delta = SceneGraphDelta {
3393 new_graph: self.scene_graph_for_ui(),
3394 invalidates_ids: true,
3395 new_objects: Vec::new(),
3396 exec_outcome: outcome,
3397 };
3398 Ok((src_delta, scene_graph_delta))
3399 }
3400
3401 fn point_id_to_ast_reference(
3406 &self,
3407 point_id: ObjectId,
3408 new_ast: &mut ast::Node<ast::Program>,
3409 ) -> Result<ast::Expr, KclError> {
3410 let point_object = self
3411 .scene_graph
3412 .objects
3413 .get(point_id.0)
3414 .ok_or_else(|| KclError::refactor(format!("Point not found: {point_id:?}")))?;
3415 let ObjectKind::Segment { segment: point_segment } = &point_object.kind else {
3416 return Err(KclError::refactor(format!("Object is not a segment: {point_object:?}")));
3417 };
3418 let Segment::Point(point) = point_segment else {
3419 return Err(KclError::refactor(format!(
3420 "Only points are currently supported: {point_object:?}"
3421 )));
3422 };
3423
3424 if let Some(owner_id) = point.owner {
3425 let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
3426 KclError::refactor(format!(
3427 "Owner of point not found in scene graph: point={point_id:?}, owner={owner_id:?}"
3428 ))
3429 })?;
3430 let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind else {
3431 return Err(KclError::refactor(format!(
3432 "Owner of point is not a segment, but found {}",
3433 owner_object.kind.human_friendly_kind_with_article()
3434 )));
3435 };
3436
3437 match owner_segment {
3438 Segment::Line(line) => {
3439 let property = if line.start == point_id {
3440 LINE_PROPERTY_START
3441 } else if line.end == point_id {
3442 LINE_PROPERTY_END
3443 } else {
3444 return Err(KclError::refactor(format!(
3445 "Internal: Point is not part of owner's line segment: point={point_id:?}, line={owner_id:?}"
3446 )));
3447 };
3448 get_or_insert_ast_reference(new_ast, &owner_object.source, LINE_VARIABLE, Some(property))
3449 }
3450 Segment::Arc(arc) => {
3451 let property = if arc.start == point_id {
3452 ARC_PROPERTY_START
3453 } else if arc.end == point_id {
3454 ARC_PROPERTY_END
3455 } else if arc.center == point_id {
3456 ARC_PROPERTY_CENTER
3457 } else {
3458 return Err(KclError::refactor(format!(
3459 "Internal: Point is not part of owner's arc segment: point={point_id:?}, arc={owner_id:?}"
3460 )));
3461 };
3462 get_or_insert_ast_reference(new_ast, &owner_object.source, ARC_VARIABLE, Some(property))
3463 }
3464 Segment::Circle(circle) => {
3465 let property = if circle.start == point_id {
3466 CIRCLE_PROPERTY_START
3467 } else if circle.center == point_id {
3468 CIRCLE_PROPERTY_CENTER
3469 } else {
3470 return Err(KclError::refactor(format!(
3471 "Internal: Point is not part of owner's circle segment: point={point_id:?}, circle={owner_id:?}"
3472 )));
3473 };
3474 get_or_insert_ast_reference(new_ast, &owner_object.source, CIRCLE_VARIABLE, Some(property))
3475 }
3476 Segment::ControlPointSpline(spline) => {
3477 let Some(index) = spline.controls.iter().position(|id| *id == point_id) else {
3478 return Err(KclError::refactor(format!(
3479 "Internal: Point is not part of owner's controlPointSpline segment: point={point_id:?}, spline={owner_id:?}"
3480 )));
3481 };
3482 let owner_expr =
3483 get_or_insert_ast_reference(new_ast, &owner_object.source, CONTROL_POINT_SPLINE_FN, None)?;
3484 let controls_expr = create_member_expression(owner_expr, CONTROL_POINT_SPLINE_PROPERTY_CONTROLS);
3485 Ok(create_index_expression(controls_expr, index))
3486 }
3487 _ => Err(KclError::refactor(format!(
3488 "Internal: Owner of point is not a supported segment type for constraints: {owner_segment:?}"
3489 ))),
3490 }
3491 } else {
3492 get_or_insert_ast_reference(new_ast, &point_object.source, "point", None)
3494 }
3495 }
3496
3497 fn line_id_to_ast_reference(
3498 &self,
3499 line_id: ObjectId,
3500 new_ast: &mut ast::Node<ast::Program>,
3501 ) -> Result<ast::Expr, KclError> {
3502 let line_object = self
3503 .scene_graph
3504 .objects
3505 .get(line_id.0)
3506 .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3507 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3508 return Err(KclError::refactor(format!("Object is not a segment: {line_object:?}")));
3509 };
3510 let Segment::Line(line) = line_segment else {
3511 return Err(KclError::refactor(format!(
3512 "Only lines are currently supported: {line_object:?}"
3513 )));
3514 };
3515
3516 if let Some(owner_id) = line.owner {
3517 let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
3518 KclError::refactor(format!(
3519 "Owner of line not found in scene graph: line={line_id:?}, owner={owner_id:?}"
3520 ))
3521 })?;
3522 let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind else {
3523 return Err(KclError::refactor(format!(
3524 "Owner of line is not a segment, but found {}",
3525 owner_object.kind.human_friendly_kind_with_article()
3526 )));
3527 };
3528
3529 match owner_segment {
3530 Segment::ControlPointSpline(spline) => {
3531 let Some(index) = spline
3532 .controls
3533 .windows(2)
3534 .position(|window| window[0] == line.start && window[1] == line.end)
3535 else {
3536 return Err(KclError::refactor(format!(
3537 "Internal: Line is not part of owner's controlPointSpline segment: line={line_id:?}, spline={owner_id:?}"
3538 )));
3539 };
3540 let owner_expr =
3541 get_or_insert_ast_reference(new_ast, &owner_object.source, CONTROL_POINT_SPLINE_FN, None)?;
3542 let edges_expr = create_member_expression(owner_expr, CONTROL_POINT_SPLINE_PROPERTY_EDGES);
3543 Ok(create_index_expression(edges_expr, index))
3544 }
3545 _ => Err(KclError::refactor(format!(
3546 "Internal: Owner of line is not a supported segment type for constraints: {owner_segment:?}"
3547 ))),
3548 }
3549 } else {
3550 get_or_insert_ast_reference(new_ast, &line_object.source, "line", None)
3551 }
3552 }
3553
3554 fn coincident_segment_to_ast(
3555 &self,
3556 segment: &ConstraintSegment,
3557 new_ast: &mut ast::Node<ast::Program>,
3558 ) -> Result<ast::Expr, KclError> {
3559 match segment {
3560 ConstraintSegment::Origin(_) => Ok(ast_name_expr("ORIGIN".to_owned())),
3561 ConstraintSegment::Segment(segment_id) => self.segment_id_to_constraint_ast_reference(*segment_id, new_ast),
3562 }
3563 }
3564
3565 fn segment_id_to_constraint_ast_reference(
3566 &self,
3567 segment_id: ObjectId,
3568 new_ast: &mut ast::Node<ast::Program>,
3569 ) -> Result<ast::Expr, KclError> {
3570 let segment_object = self
3571 .scene_graph
3572 .objects
3573 .get(segment_id.0)
3574 .ok_or_else(|| KclError::refactor(format!("Object not found: {segment_id:?}")))?;
3575 let ObjectKind::Segment { segment } = &segment_object.kind else {
3576 return Err(KclError::refactor(format!(
3577 "Object is not a segment, it is {}",
3578 segment_object.kind.human_friendly_kind_with_article()
3579 )));
3580 };
3581
3582 match segment {
3583 Segment::Point(_) => self.point_id_to_ast_reference(segment_id, new_ast),
3584 Segment::Line(_) => self.line_id_to_ast_reference(segment_id, new_ast),
3585 Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, "arc", None),
3586 Segment::Circle(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, CIRCLE_VARIABLE, None),
3587 Segment::ControlPointSpline(_) => {
3588 get_or_insert_ast_reference(new_ast, &segment_object.source, CONTROL_POINT_SPLINE_FN, None)
3589 }
3590 }
3591 }
3592
3593 fn axis_constraint_segment_to_ast(
3594 &self,
3595 segment: &ConstraintSegment,
3596 new_ast: &mut ast::Node<ast::Program>,
3597 ) -> Result<ast::Expr, KclError> {
3598 match segment {
3599 ConstraintSegment::Origin(_) => Ok(ast_name_expr("ORIGIN".to_owned())),
3600 ConstraintSegment::Segment(point_id) => self.point_id_to_ast_reference(*point_id, new_ast),
3601 }
3602 }
3603
3604 async fn add_coincident(
3605 &mut self,
3606 sketch: ObjectId,
3607 coincident: Coincident,
3608 new_ast: &mut ast::Node<ast::Program>,
3609 ) -> Result<AstNodeRef, KclError> {
3610 let sketch_id = sketch;
3611 for segment in &coincident.segments {
3612 let ConstraintSegment::Segment(segment_id) = segment else {
3613 continue;
3614 };
3615 let Some(segment_object) = self.scene_graph.objects.get(segment_id.0) else {
3616 continue;
3617 };
3618 if matches!(
3619 segment_object.kind,
3620 ObjectKind::Segment {
3621 segment: Segment::ControlPointSpline(_)
3622 }
3623 ) {
3624 return Err(KclError::refactor(
3625 "Coincident with a full controlPointSpline is not supported yet. Constrain a control point or spline edge instead."
3626 .to_owned(),
3627 ));
3628 }
3629 }
3630 let segment_asts = coincident
3631 .segments
3632 .iter()
3633 .map(|segment| self.coincident_segment_to_ast(segment, new_ast))
3634 .collect::<Result<Vec<_>, _>>()?;
3635 if segment_asts.len() < 2 {
3636 return Err(KclError::refactor(format!(
3637 "Coincident constraint must have at least 2 inputs, got {}",
3638 segment_asts.len()
3639 )));
3640 }
3641
3642 let coincident_ast = create_coincident_ast(segment_asts);
3644
3645 let (sketch_block_ref, _) = self.mutate_ast(
3647 new_ast,
3648 sketch_id,
3649 AstMutateCommand::AddSketchBlockExprStmt { expr: coincident_ast },
3650 )?;
3651 Ok(sketch_block_ref)
3652 }
3653
3654 async fn add_distance(
3655 &mut self,
3656 sketch: ObjectId,
3657 distance: Distance,
3658 new_ast: &mut ast::Node<ast::Program>,
3659 ) -> Result<AstNodeRef, KclError> {
3660 let sketch_id = sketch;
3661 let [pt0_ast, pt1_ast] = match distance.points.as_slice() {
3662 [pt0, pt1] => [
3663 self.coincident_segment_to_ast(pt0, new_ast)?,
3664 self.coincident_segment_to_ast(pt1, new_ast)?,
3665 ],
3666 _ => {
3667 return Err(KclError::refactor(format!(
3668 "Distance constraint must have exactly 2 points, got {}",
3669 distance.points.len()
3670 )));
3671 }
3672 };
3673
3674 let arguments = match &distance.label_position {
3675 Some(label_position) => vec![ast::LabeledArg {
3676 label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
3677 arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
3678 }],
3679 None => Default::default(),
3680 };
3681
3682 let distance_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3684 callee: ast::Node::no_src(ast_sketch2_name(DISTANCE_FN)),
3685 unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
3686 ast::ArrayExpression {
3687 elements: vec![pt0_ast, pt1_ast],
3688 digest: None,
3689 non_code_meta: Default::default(),
3690 },
3691 )))),
3692 arguments,
3693 digest: None,
3694 non_code_meta: Default::default(),
3695 })));
3696 let distance_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
3697 left: distance_call_ast,
3698 operator: ast::BinaryOperator::Eq,
3699 right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
3700 value: ast::LiteralValue::Number {
3701 value: distance.distance.value,
3702 suffix: distance.distance.units,
3703 },
3704 raw: format_number_literal(distance.distance.value, distance.distance.units, None).map_err(|_| {
3705 KclError::refactor(format!(
3706 "Could not format numeric suffix: {:?}",
3707 distance.distance.units
3708 ))
3709 })?,
3710 digest: None,
3711 }))),
3712 digest: None,
3713 })));
3714
3715 let (sketch_block_ref, _) = self.mutate_ast(
3717 new_ast,
3718 sketch_id,
3719 AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
3720 )?;
3721 Ok(sketch_block_ref)
3722 }
3723
3724 async fn add_angle(
3725 &mut self,
3726 sketch: ObjectId,
3727 angle: Angle,
3728 new_ast: &mut ast::Node<ast::Program>,
3729 ) -> Result<AstNodeRef, KclError> {
3730 let &[l0_id, l1_id] = angle.lines.as_slice() else {
3731 return Err(KclError::refactor(format!(
3732 "Angle constraint must have exactly 2 lines, got {}",
3733 angle.lines.len()
3734 )));
3735 };
3736 let sketch_id = sketch;
3737
3738 let line0_object = self
3740 .scene_graph
3741 .objects
3742 .get(l0_id.0)
3743 .ok_or_else(|| KclError::refactor(format!("Line not found: {l0_id:?}")))?;
3744 let ObjectKind::Segment { segment: line0_segment } = &line0_object.kind else {
3745 return Err(KclError::refactor(format!("Object is not a segment: {line0_object:?}")));
3746 };
3747 let Segment::Line(_) = line0_segment else {
3748 return Err(KclError::refactor(format!(
3749 "Only lines can be constrained to meet at an angle: {line0_object:?}",
3750 )));
3751 };
3752 let l0_ast = self.line_id_to_ast_reference(l0_id, new_ast)?;
3753
3754 let line1_object = self
3755 .scene_graph
3756 .objects
3757 .get(l1_id.0)
3758 .ok_or_else(|| KclError::refactor(format!("Line not found: {l1_id:?}")))?;
3759 let ObjectKind::Segment { segment: line1_segment } = &line1_object.kind else {
3760 return Err(KclError::refactor(format!("Object is not a segment: {line1_object:?}")));
3761 };
3762 let Segment::Line(_) = line1_segment else {
3763 return Err(KclError::refactor(format!(
3764 "Only lines can be constrained to meet at an angle: {line1_object:?}",
3765 )));
3766 };
3767 let l1_ast = self.line_id_to_ast_reference(l1_id, new_ast)?;
3768
3769 let angle_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3771 callee: ast::Node::no_src(ast_sketch2_name(ANGLE_FN)),
3772 unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
3773 ast::ArrayExpression {
3774 elements: vec![l0_ast, l1_ast],
3775 digest: None,
3776 non_code_meta: Default::default(),
3777 },
3778 )))),
3779 arguments: Default::default(),
3780 digest: None,
3781 non_code_meta: Default::default(),
3782 })));
3783 let angle_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
3784 left: angle_call_ast,
3785 operator: ast::BinaryOperator::Eq,
3786 right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
3787 value: ast::LiteralValue::Number {
3788 value: angle.angle.value,
3789 suffix: angle.angle.units,
3790 },
3791 raw: format_number_literal(angle.angle.value, angle.angle.units, None).map_err(|_| {
3792 KclError::refactor(format!("Could not format numeric suffix: {:?}", angle.angle.units))
3793 })?,
3794 digest: None,
3795 }))),
3796 digest: None,
3797 })));
3798
3799 let (sketch_block_ref, _) = self.mutate_ast(
3801 new_ast,
3802 sketch_id,
3803 AstMutateCommand::AddSketchBlockExprStmt { expr: angle_ast },
3804 )?;
3805 Ok(sketch_block_ref)
3806 }
3807
3808 async fn add_tangent(
3809 &mut self,
3810 sketch: ObjectId,
3811 tangent: Tangent,
3812 new_ast: &mut ast::Node<ast::Program>,
3813 ) -> Result<AstNodeRef, KclError> {
3814 let &[seg0_id, seg1_id] = tangent.input.as_slice() else {
3815 return Err(KclError::refactor(format!(
3816 "Tangent constraint must have exactly 2 segments, got {}",
3817 tangent.input.len()
3818 )));
3819 };
3820 let sketch_id = sketch;
3821
3822 let seg0_object = self
3823 .scene_graph
3824 .objects
3825 .get(seg0_id.0)
3826 .ok_or_else(|| KclError::refactor(format!("Segment not found: {seg0_id:?}")))?;
3827 let ObjectKind::Segment { segment: seg0_segment } = &seg0_object.kind else {
3828 return Err(KclError::refactor(format!("Object is not a segment: {seg0_object:?}")));
3829 };
3830 let seg0_ast = match seg0_segment {
3831 Segment::Line(_) | Segment::Arc(_) | Segment::Circle(_) => {
3832 self.segment_id_to_constraint_ast_reference(seg0_id, new_ast)?
3833 }
3834 _ => {
3835 return Err(KclError::refactor(format!(
3836 "Tangent supports only line/arc/circle segments for now, got: {seg0_segment:?}"
3837 )));
3838 }
3839 };
3840
3841 let seg1_object = self
3842 .scene_graph
3843 .objects
3844 .get(seg1_id.0)
3845 .ok_or_else(|| KclError::refactor(format!("Segment not found: {seg1_id:?}")))?;
3846 let ObjectKind::Segment { segment: seg1_segment } = &seg1_object.kind else {
3847 return Err(KclError::refactor(format!("Object is not a segment: {seg1_object:?}")));
3848 };
3849 let seg1_ast = match seg1_segment {
3850 Segment::Line(_) | Segment::Arc(_) | Segment::Circle(_) => {
3851 self.segment_id_to_constraint_ast_reference(seg1_id, new_ast)?
3852 }
3853 _ => {
3854 return Err(KclError::refactor(format!(
3855 "Tangent supports only line/arc/circle segments for now, got: {seg1_segment:?}"
3856 )));
3857 }
3858 };
3859
3860 let tangent_ast = create_tangent_ast(seg0_ast, seg1_ast);
3861 let (sketch_block_ref, _) = self.mutate_ast(
3862 new_ast,
3863 sketch_id,
3864 AstMutateCommand::AddSketchBlockExprStmt { expr: tangent_ast },
3865 )?;
3866 Ok(sketch_block_ref)
3867 }
3868
3869 async fn add_symmetric(
3870 &mut self,
3871 sketch: ObjectId,
3872 symmetric: Symmetric,
3873 new_ast: &mut ast::Node<ast::Program>,
3874 ) -> Result<AstNodeRef, KclError> {
3875 let &[input0_id, input1_id] = symmetric.input.as_slice() else {
3876 return Err(KclError::refactor(format!(
3877 "Symmetric constraint must have exactly 2 inputs, got {}",
3878 symmetric.input.len()
3879 )));
3880 };
3881 let sketch_id = sketch;
3882
3883 let input0_ast = self.symmetric_input_id_to_ast_reference(input0_id, new_ast)?;
3884 let input1_ast = self.symmetric_input_id_to_ast_reference(input1_id, new_ast)?;
3885 let axis_ast = self.symmetric_axis_id_to_ast_reference(symmetric.axis, new_ast)?;
3886
3887 let symmetric_ast = create_symmetric_ast(vec![input0_ast, input1_ast], axis_ast);
3888 let (sketch_block_ref, _) = self.mutate_ast(
3889 new_ast,
3890 sketch_id,
3891 AstMutateCommand::AddSketchBlockExprStmt { expr: symmetric_ast },
3892 )?;
3893 Ok(sketch_block_ref)
3894 }
3895
3896 async fn add_midpoint(
3897 &mut self,
3898 sketch: ObjectId,
3899 midpoint: Midpoint,
3900 new_ast: &mut ast::Node<ast::Program>,
3901 ) -> Result<AstNodeRef, KclError> {
3902 let sketch_id = sketch;
3903 let point_ast = self.axis_constraint_segment_to_ast(&midpoint.point, new_ast)?;
3904
3905 let segment_object = self
3906 .scene_graph
3907 .objects
3908 .get(midpoint.segment.0)
3909 .ok_or_else(|| KclError::refactor(format!("Segment not found: {:?}", midpoint.segment)))?;
3910 let ObjectKind::Segment {
3911 segment: midpoint_segment,
3912 } = &segment_object.kind
3913 else {
3914 return Err(KclError::refactor(format!(
3915 "Object must be a segment, but it was {}",
3916 segment_object.kind.human_friendly_kind_with_article()
3917 )));
3918 };
3919 let segment_ast = match midpoint_segment {
3920 Segment::Line(_) => self.line_id_to_ast_reference(midpoint.segment, new_ast)?,
3921 Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, "arc", None)?,
3922 _ => {
3923 return Err(KclError::refactor(format!(
3924 "Midpoint target must be a line or arc segment but it was {}",
3925 midpoint_segment.human_friendly_kind_with_article()
3926 )));
3927 }
3928 };
3929
3930 let midpoint_ast = create_midpoint_ast(segment_ast, point_ast);
3931 let (sketch_block_ref, _) = self.mutate_ast(
3932 new_ast,
3933 sketch_id,
3934 AstMutateCommand::AddSketchBlockExprStmt { expr: midpoint_ast },
3935 )?;
3936 Ok(sketch_block_ref)
3937 }
3938
3939 async fn add_equal_radius(
3940 &mut self,
3941 sketch: ObjectId,
3942 equal_radius: EqualRadius,
3943 new_ast: &mut ast::Node<ast::Program>,
3944 ) -> Result<AstNodeRef, KclError> {
3945 if equal_radius.input.len() < 2 {
3946 return Err(KclError::refactor(format!(
3947 "equalRadius constraint must have at least 2 segments, got {}",
3948 equal_radius.input.len()
3949 )));
3950 }
3951
3952 let sketch_id = sketch;
3953 let input_asts = equal_radius
3954 .input
3955 .iter()
3956 .map(|segment_id| self.equal_radius_segment_id_to_ast_reference(*segment_id, new_ast))
3957 .collect::<Result<Vec<_>, _>>()?;
3958
3959 let equal_radius_ast = create_equal_radius_ast(input_asts);
3960 let (sketch_block_ref, _) = self.mutate_ast(
3961 new_ast,
3962 sketch_id,
3963 AstMutateCommand::AddSketchBlockExprStmt { expr: equal_radius_ast },
3964 )?;
3965 Ok(sketch_block_ref)
3966 }
3967
3968 async fn add_radius(
3969 &mut self,
3970 sketch: ObjectId,
3971 radius: Radius,
3972 new_ast: &mut ast::Node<ast::Program>,
3973 ) -> Result<AstNodeRef, KclError> {
3974 let params = ArcSizeConstraintParams {
3975 points: vec![radius.arc],
3976 function_name: RADIUS_FN,
3977 value: radius.radius.value,
3978 units: radius.radius.units,
3979 label_position: radius.label_position,
3980 constraint_type_name: "Radius",
3981 };
3982 self.add_arc_size_constraint(sketch, params, new_ast).await
3983 }
3984
3985 async fn add_diameter(
3986 &mut self,
3987 sketch: ObjectId,
3988 diameter: Diameter,
3989 new_ast: &mut ast::Node<ast::Program>,
3990 ) -> Result<AstNodeRef, KclError> {
3991 let params = ArcSizeConstraintParams {
3992 points: vec![diameter.arc],
3993 function_name: DIAMETER_FN,
3994 value: diameter.diameter.value,
3995 units: diameter.diameter.units,
3996 label_position: diameter.label_position,
3997 constraint_type_name: "Diameter",
3998 };
3999 self.add_arc_size_constraint(sketch, params, new_ast).await
4000 }
4001
4002 async fn add_fixed_constraints(
4003 &mut self,
4004 sketch: ObjectId,
4005 points: Vec<FixedPoint>,
4006 new_ast: &mut ast::Node<ast::Program>,
4007 ) -> Result<AstNodeRef, KclError> {
4008 let mut sketch_block_ref = None;
4009
4010 for fixed_point in points {
4011 let point_ast = self.point_id_to_ast_reference(fixed_point.point, new_ast)?;
4012 let fixed_ast = create_fixed_point_constraint_ast(point_ast, fixed_point.position)
4013 .map_err(|err| KclError::refactor(err.to_string()))?;
4014
4015 let (sketch_ref, _) = self.mutate_ast(
4016 new_ast,
4017 sketch,
4018 AstMutateCommand::AddSketchBlockExprStmt { expr: fixed_ast },
4019 )?;
4020 sketch_block_ref = Some(sketch_ref);
4021 }
4022
4023 sketch_block_ref.ok_or_else(|| KclError::refactor("Fixed constraint requires at least one point".to_owned()))
4024 }
4025
4026 async fn add_arc_size_constraint(
4027 &mut self,
4028 sketch: ObjectId,
4029 params: ArcSizeConstraintParams,
4030 new_ast: &mut ast::Node<ast::Program>,
4031 ) -> Result<AstNodeRef, KclError> {
4032 let sketch_id = sketch;
4033
4034 if params.points.len() != 1 {
4036 return Err(KclError::refactor(format!(
4037 "{} constraint must have exactly 1 argument (an arc segment), got {}",
4038 params.constraint_type_name,
4039 params.points.len()
4040 )));
4041 }
4042
4043 let arc_id = params.points[0];
4044 let arc_object = self
4045 .scene_graph
4046 .objects
4047 .get(arc_id.0)
4048 .ok_or_else(|| KclError::refactor(format!("Arc segment not found: {arc_id:?}")))?;
4049 let ObjectKind::Segment { segment: arc_segment } = &arc_object.kind else {
4050 return Err(KclError::refactor(format!("Object is not a segment: {arc_object:?}")));
4051 };
4052 let ref_type = match arc_segment {
4053 Segment::Arc(_) => ARC_VARIABLE,
4054 Segment::Circle(_) => CIRCLE_VARIABLE,
4055 _ => {
4056 return Err(KclError::refactor(format!(
4057 "{} constraint argument must be an arc or circle segment, got: {arc_segment:?}",
4058 params.constraint_type_name
4059 )));
4060 }
4061 };
4062 let arc_ast = get_or_insert_ast_reference(new_ast, &arc_object.source, ref_type, None)?;
4064 let arguments = match ¶ms.label_position {
4065 Some(label_position) => vec![ast::LabeledArg {
4066 label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
4067 arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
4068 }],
4069 None => Default::default(),
4070 };
4071
4072 let call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
4074 callee: ast::Node::no_src(ast_sketch2_name(params.function_name)),
4075 unlabeled: Some(arc_ast),
4076 arguments,
4077 digest: None,
4078 non_code_meta: Default::default(),
4079 })));
4080 let constraint_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
4081 left: call_ast,
4082 operator: ast::BinaryOperator::Eq,
4083 right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
4084 value: ast::LiteralValue::Number {
4085 value: params.value,
4086 suffix: params.units,
4087 },
4088 raw: format_number_literal(params.value, params.units, None)
4089 .map_err(|_| KclError::refactor(format!("Could not format numeric suffix: {:?}", params.units)))?,
4090 digest: None,
4091 }))),
4092 digest: None,
4093 })));
4094
4095 let (sketch_block_ref, _) = self.mutate_ast(
4097 new_ast,
4098 sketch_id,
4099 AstMutateCommand::AddSketchBlockExprStmt { expr: constraint_ast },
4100 )?;
4101 Ok(sketch_block_ref)
4102 }
4103
4104 async fn add_horizontal_distance(
4105 &mut self,
4106 sketch: ObjectId,
4107 distance: Distance,
4108 new_ast: &mut ast::Node<ast::Program>,
4109 ) -> Result<AstNodeRef, KclError> {
4110 let sketch_id = sketch;
4111 let [pt0_ast, pt1_ast] = match distance.points.as_slice() {
4112 [pt0, pt1] => [
4113 self.coincident_segment_to_ast(pt0, new_ast)?,
4114 self.coincident_segment_to_ast(pt1, new_ast)?,
4115 ],
4116 _ => {
4117 return Err(KclError::refactor(format!(
4118 "Horizontal distance constraint must have exactly 2 points, got {}",
4119 distance.points.len()
4120 )));
4121 }
4122 };
4123
4124 let arguments = match &distance.label_position {
4125 Some(label_position) => vec![ast::LabeledArg {
4126 label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
4127 arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
4128 }],
4129 None => Default::default(),
4130 };
4131
4132 let distance_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
4134 callee: ast::Node::no_src(ast_sketch2_name(HORIZONTAL_DISTANCE_FN)),
4135 unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
4136 ast::ArrayExpression {
4137 elements: vec![pt0_ast, pt1_ast],
4138 digest: None,
4139 non_code_meta: Default::default(),
4140 },
4141 )))),
4142 arguments,
4143 digest: None,
4144 non_code_meta: Default::default(),
4145 })));
4146 let distance_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
4147 left: distance_call_ast,
4148 operator: ast::BinaryOperator::Eq,
4149 right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
4150 value: ast::LiteralValue::Number {
4151 value: distance.distance.value,
4152 suffix: distance.distance.units,
4153 },
4154 raw: format_number_literal(distance.distance.value, distance.distance.units, None).map_err(|_| {
4155 KclError::refactor(format!(
4156 "Could not format numeric suffix: {:?}",
4157 distance.distance.units
4158 ))
4159 })?,
4160 digest: None,
4161 }))),
4162 digest: None,
4163 })));
4164
4165 let (sketch_block_ref, _) = self.mutate_ast(
4167 new_ast,
4168 sketch_id,
4169 AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
4170 )?;
4171 Ok(sketch_block_ref)
4172 }
4173
4174 async fn add_vertical_distance(
4175 &mut self,
4176 sketch: ObjectId,
4177 distance: Distance,
4178 new_ast: &mut ast::Node<ast::Program>,
4179 ) -> Result<AstNodeRef, KclError> {
4180 let sketch_id = sketch;
4181 let [pt0_ast, pt1_ast] = match distance.points.as_slice() {
4182 [pt0, pt1] => [
4183 self.coincident_segment_to_ast(pt0, new_ast)?,
4184 self.coincident_segment_to_ast(pt1, new_ast)?,
4185 ],
4186 _ => {
4187 return Err(KclError::refactor(format!(
4188 "Vertical distance constraint must have exactly 2 points, got {}",
4189 distance.points.len()
4190 )));
4191 }
4192 };
4193
4194 let arguments = match &distance.label_position {
4195 Some(label_position) => vec![ast::LabeledArg {
4196 label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
4197 arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
4198 }],
4199 None => Default::default(),
4200 };
4201
4202 let distance_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
4204 callee: ast::Node::no_src(ast_sketch2_name(VERTICAL_DISTANCE_FN)),
4205 unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
4206 ast::ArrayExpression {
4207 elements: vec![pt0_ast, pt1_ast],
4208 digest: None,
4209 non_code_meta: Default::default(),
4210 },
4211 )))),
4212 arguments,
4213 digest: None,
4214 non_code_meta: Default::default(),
4215 })));
4216 let distance_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
4217 left: distance_call_ast,
4218 operator: ast::BinaryOperator::Eq,
4219 right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
4220 value: ast::LiteralValue::Number {
4221 value: distance.distance.value,
4222 suffix: distance.distance.units,
4223 },
4224 raw: format_number_literal(distance.distance.value, distance.distance.units, None).map_err(|_| {
4225 KclError::refactor(format!(
4226 "Could not format numeric suffix: {:?}",
4227 distance.distance.units
4228 ))
4229 })?,
4230 digest: None,
4231 }))),
4232 digest: None,
4233 })));
4234
4235 let (sketch_block_ref, _) = self.mutate_ast(
4237 new_ast,
4238 sketch_id,
4239 AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
4240 )?;
4241 Ok(sketch_block_ref)
4242 }
4243
4244 async fn add_horizontal(
4245 &mut self,
4246 sketch: ObjectId,
4247 horizontal: Horizontal,
4248 new_ast: &mut ast::Node<ast::Program>,
4249 ) -> Result<AstNodeRef, KclError> {
4250 let sketch_id = sketch;
4251
4252 let first_arg_ast = match horizontal {
4254 Horizontal::Line { line } => {
4255 let line_object = self
4256 .scene_graph
4257 .objects
4258 .get(line.0)
4259 .ok_or_else(|| KclError::refactor(format!("Line not found: {line:?}")))?;
4260 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4261 let kind = line_object.kind.human_friendly_kind_with_article();
4262 return Err(KclError::refactor(format!(
4263 "This constraint only works on Segments, but you selected {kind}"
4264 )));
4265 };
4266 let Segment::Line(_) = line_segment else {
4267 return Err(KclError::refactor(format!(
4268 "Only lines can be made horizontal, but you selected {}",
4269 line_segment.human_friendly_kind_with_article(),
4270 )));
4271 };
4272 self.line_id_to_ast_reference(line, new_ast)?
4273 }
4274 Horizontal::Points { points } => {
4275 let point_asts = points
4276 .iter()
4277 .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
4278 .collect::<Result<Vec<_>, _>>()?;
4279 ast::ArrayExpression::new(point_asts).into()
4280 }
4281 };
4282 let horizontal_ast = create_horizontal_ast(first_arg_ast);
4284
4285 let (sketch_block_ref, _) = self.mutate_ast(
4287 new_ast,
4288 sketch_id,
4289 AstMutateCommand::AddSketchBlockExprStmt { expr: horizontal_ast },
4290 )?;
4291 Ok(sketch_block_ref)
4292 }
4293
4294 async fn add_lines_equal_length(
4295 &mut self,
4296 sketch: ObjectId,
4297 lines_equal_length: LinesEqualLength,
4298 new_ast: &mut ast::Node<ast::Program>,
4299 ) -> Result<AstNodeRef, KclError> {
4300 if lines_equal_length.lines.len() < 2 {
4301 return Err(KclError::refactor(format!(
4302 "Lines equal length constraint must have at least 2 lines, got {}",
4303 lines_equal_length.lines.len()
4304 )));
4305 };
4306
4307 let sketch_id = sketch;
4308
4309 let line_asts = lines_equal_length
4311 .lines
4312 .iter()
4313 .map(|line_id| {
4314 let line_object = self
4315 .scene_graph
4316 .objects
4317 .get(line_id.0)
4318 .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
4319 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4320 let kind = line_object.kind.human_friendly_kind_with_article();
4321 return Err(KclError::refactor(format!(
4322 "This constraint only works on Segments, but you selected {kind}"
4323 )));
4324 };
4325 let Segment::Line(_) = line_segment else {
4326 let kind = line_segment.human_friendly_kind_with_article();
4327 return Err(KclError::refactor(format!(
4328 "Only lines can be made equal length, but you selected {kind}"
4329 )));
4330 };
4331
4332 self.line_id_to_ast_reference(*line_id, new_ast)
4333 })
4334 .collect::<Result<Vec<_>, _>>()?;
4335
4336 let equal_length_ast = create_equal_length_ast(line_asts);
4338
4339 let (sketch_block_ref, _) = self.mutate_ast(
4341 new_ast,
4342 sketch_id,
4343 AstMutateCommand::AddSketchBlockExprStmt { expr: equal_length_ast },
4344 )?;
4345 Ok(sketch_block_ref)
4346 }
4347
4348 fn equal_radius_segment_id_to_ast_reference(
4349 &mut self,
4350 segment_id: ObjectId,
4351 new_ast: &mut ast::Node<ast::Program>,
4352 ) -> Result<ast::Expr, KclError> {
4353 let segment_object = self
4354 .scene_graph
4355 .objects
4356 .get(segment_id.0)
4357 .ok_or_else(|| KclError::refactor(format!("Segment not found: {segment_id:?}")))?;
4358 let ObjectKind::Segment { segment } = &segment_object.kind else {
4359 return Err(KclError::refactor(format!(
4360 "Object is not a segment, it was {}",
4361 segment_object.kind.human_friendly_kind_with_article()
4362 )));
4363 };
4364
4365 let ref_type = match segment {
4366 Segment::Arc(_) => ARC_VARIABLE,
4367 Segment::Circle(_) => CIRCLE_VARIABLE,
4368 _ => {
4369 return Err(KclError::refactor(format!(
4370 "equalRadius supports only arc/circle segments, got {}",
4371 segment.human_friendly_kind_with_article()
4372 )));
4373 }
4374 };
4375
4376 get_or_insert_ast_reference(new_ast, &segment_object.source, ref_type, None)
4377 }
4378
4379 fn symmetric_input_id_to_ast_reference(
4380 &mut self,
4381 segment_id: ObjectId,
4382 new_ast: &mut ast::Node<ast::Program>,
4383 ) -> Result<ast::Expr, KclError> {
4384 let segment_object = self
4385 .scene_graph
4386 .objects
4387 .get(segment_id.0)
4388 .ok_or_else(|| KclError::refactor(format!("Segment not found: {segment_id:?}")))?;
4389 let ObjectKind::Segment { segment } = &segment_object.kind else {
4390 return Err(KclError::refactor(format!(
4391 "Object is not a segment, it was {}",
4392 segment_object.kind.human_friendly_kind_with_article()
4393 )));
4394 };
4395
4396 match segment {
4397 Segment::Point(_) => self.point_id_to_ast_reference(segment_id, new_ast),
4398 Segment::Line(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, LINE_VARIABLE, None),
4399 Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, ARC_VARIABLE, None),
4400 Segment::Circle(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, CIRCLE_VARIABLE, None),
4401 Segment::ControlPointSpline(_) => Err(KclError::refactor(
4402 "Symmetric does not yet support control point splines".to_owned(),
4403 )),
4404 }
4405 }
4406
4407 fn symmetric_axis_id_to_ast_reference(
4408 &mut self,
4409 segment_id: ObjectId,
4410 new_ast: &mut ast::Node<ast::Program>,
4411 ) -> Result<ast::Expr, KclError> {
4412 let segment_object = self
4413 .scene_graph
4414 .objects
4415 .get(segment_id.0)
4416 .ok_or_else(|| KclError::refactor(format!("Axis segment not found: {segment_id:?}")))?;
4417 let ObjectKind::Segment { segment } = &segment_object.kind else {
4418 return Err(KclError::refactor(format!(
4419 "Object is not a segment, it was {}",
4420 segment_object.kind.human_friendly_kind_with_article()
4421 )));
4422 };
4423 match segment {
4424 Segment::Line(_) => self.line_id_to_ast_reference(segment_id, new_ast),
4425 _ => Err(KclError::refactor(format!(
4426 "Symmetric axis must be a line, got {}",
4427 segment.human_friendly_kind_with_article()
4428 ))),
4429 }
4430 }
4431
4432 async fn add_parallel(
4433 &mut self,
4434 sketch: ObjectId,
4435 parallel: Parallel,
4436 new_ast: &mut ast::Node<ast::Program>,
4437 ) -> Result<AstNodeRef, KclError> {
4438 if parallel.lines.len() < 2 {
4439 return Err(KclError::refactor(format!(
4440 "Parallel constraint must have at least 2 lines, got {}",
4441 parallel.lines.len()
4442 )));
4443 };
4444
4445 let sketch_id = sketch;
4446
4447 let line_asts = parallel
4448 .lines
4449 .iter()
4450 .map(|line_id| {
4451 let line_object = self
4452 .scene_graph
4453 .objects
4454 .get(line_id.0)
4455 .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
4456 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4457 let kind = line_object.kind.human_friendly_kind_with_article();
4458 return Err(KclError::refactor(format!(
4459 "This constraint only works on Segments, but you selected {kind}"
4460 )));
4461 };
4462 let Segment::Line(_) = line_segment else {
4463 let kind = line_segment.human_friendly_kind_with_article();
4464 return Err(KclError::refactor(format!(
4465 "Only lines can be made parallel, but you selected {kind}"
4466 )));
4467 };
4468
4469 self.line_id_to_ast_reference(*line_id, new_ast)
4470 })
4471 .collect::<Result<Vec<_>, _>>()?;
4472
4473 let call_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
4474 callee: ast::Node::no_src(ast_sketch2_name(LinesAtAngleKind::Parallel.to_function_name())),
4475 unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
4476 ast::ArrayExpression {
4477 elements: line_asts,
4478 digest: None,
4479 non_code_meta: Default::default(),
4480 },
4481 )))),
4482 arguments: Default::default(),
4483 digest: None,
4484 non_code_meta: Default::default(),
4485 })));
4486
4487 let (sketch_block_ref, _) = self.mutate_ast(
4488 new_ast,
4489 sketch_id,
4490 AstMutateCommand::AddSketchBlockExprStmt { expr: call_ast },
4491 )?;
4492 Ok(sketch_block_ref)
4493 }
4494
4495 async fn add_perpendicular(
4496 &mut self,
4497 sketch: ObjectId,
4498 perpendicular: Perpendicular,
4499 new_ast: &mut ast::Node<ast::Program>,
4500 ) -> Result<AstNodeRef, KclError> {
4501 self.add_lines_at_angle_constraint(sketch, LinesAtAngleKind::Perpendicular, perpendicular.lines, new_ast)
4502 .await
4503 }
4504
4505 async fn add_lines_at_angle_constraint(
4506 &mut self,
4507 sketch: ObjectId,
4508 angle_kind: LinesAtAngleKind,
4509 lines: Vec<ObjectId>,
4510 new_ast: &mut ast::Node<ast::Program>,
4511 ) -> Result<AstNodeRef, KclError> {
4512 let &[line0_id, line1_id] = lines.as_slice() else {
4513 return Err(KclError::refactor(format!(
4514 "{} constraint must have exactly 2 lines, got {}",
4515 angle_kind.to_function_name(),
4516 lines.len()
4517 )));
4518 };
4519
4520 let sketch_id = sketch;
4521
4522 let line0_object = self
4524 .scene_graph
4525 .objects
4526 .get(line0_id.0)
4527 .ok_or_else(|| KclError::refactor(format!("Line not found: {line0_id:?}")))?;
4528 let ObjectKind::Segment { segment: line0_segment } = &line0_object.kind else {
4529 let kind = line0_object.kind.human_friendly_kind_with_article();
4530 return Err(KclError::refactor(format!(
4531 "This constraint only works on Segments, but you selected {kind}"
4532 )));
4533 };
4534 let Segment::Line(_) = line0_segment else {
4535 return Err(KclError::refactor(format!(
4536 "Only lines can be made {}, but you selected {}",
4537 angle_kind.to_function_name(),
4538 line0_segment.human_friendly_kind_with_article(),
4539 )));
4540 };
4541 let line0_ast = self.line_id_to_ast_reference(line0_id, new_ast)?;
4542
4543 let line1_object = self
4544 .scene_graph
4545 .objects
4546 .get(line1_id.0)
4547 .ok_or_else(|| KclError::refactor(format!("Line not found: {line1_id:?}")))?;
4548 let ObjectKind::Segment { segment: line1_segment } = &line1_object.kind else {
4549 let kind = line1_object.kind.human_friendly_kind_with_article();
4550 return Err(KclError::refactor(format!(
4551 "This constraint only works on Segments, but you selected {kind}"
4552 )));
4553 };
4554 let Segment::Line(_) = line1_segment else {
4555 return Err(KclError::refactor(format!(
4556 "Only lines can be made {}, but you selected {}",
4557 angle_kind.to_function_name(),
4558 line1_segment.human_friendly_kind_with_article(),
4559 )));
4560 };
4561 let line1_ast = self.line_id_to_ast_reference(line1_id, new_ast)?;
4562
4563 let call_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
4565 callee: ast::Node::no_src(ast_sketch2_name(angle_kind.to_function_name())),
4566 unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
4567 ast::ArrayExpression {
4568 elements: vec![line0_ast, line1_ast],
4569 digest: None,
4570 non_code_meta: Default::default(),
4571 },
4572 )))),
4573 arguments: Default::default(),
4574 digest: None,
4575 non_code_meta: Default::default(),
4576 })));
4577
4578 let (sketch_block_ref, _) = self.mutate_ast(
4580 new_ast,
4581 sketch_id,
4582 AstMutateCommand::AddSketchBlockExprStmt { expr: call_ast },
4583 )?;
4584 Ok(sketch_block_ref)
4585 }
4586
4587 async fn add_vertical(
4588 &mut self,
4589 sketch: ObjectId,
4590 vertical: Vertical,
4591 new_ast: &mut ast::Node<ast::Program>,
4592 ) -> Result<AstNodeRef, KclError> {
4593 let sketch_id = sketch;
4594
4595 let first_arg_ast = match vertical {
4596 Vertical::Line { line } => {
4597 let line_object = self
4599 .scene_graph
4600 .objects
4601 .get(line.0)
4602 .ok_or_else(|| KclError::refactor(format!("Line not found: {line:?}")))?;
4603 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4604 let kind = line_object.kind.human_friendly_kind_with_article();
4605 return Err(KclError::refactor(format!(
4606 "This constraint only works on Segments, but you selected {kind}"
4607 )));
4608 };
4609 let Segment::Line(_) = line_segment else {
4610 return Err(KclError::refactor(format!(
4611 "Only lines can be made vertical, but you selected {}",
4612 line_segment.human_friendly_kind_with_article()
4613 )));
4614 };
4615 self.line_id_to_ast_reference(line, new_ast)?
4616 }
4617 Vertical::Points { points } => {
4618 let point_asts = points
4619 .iter()
4620 .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
4621 .collect::<Result<Vec<_>, _>>()?;
4622 ast::ArrayExpression::new(point_asts).into()
4623 }
4624 };
4625 let vertical_ast = create_vertical_ast(first_arg_ast);
4627
4628 let (sketch_block_ref, _) = self.mutate_ast(
4630 new_ast,
4631 sketch_id,
4632 AstMutateCommand::AddSketchBlockExprStmt { expr: vertical_ast },
4633 )?;
4634 Ok(sketch_block_ref)
4635 }
4636
4637 async fn execute_after_add_constraint(
4638 &mut self,
4639 ctx: &ExecutorContext,
4640 sketch_id: ObjectId,
4641 sketch_block_ref: AstNodeRef,
4642 new_ast: &mut ast::Node<ast::Program>,
4643 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
4644 let new_source = source_from_ast(new_ast);
4646 let (new_program, errors) = Program::parse(&new_source)
4648 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
4649 if !errors.is_empty() {
4650 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
4651 "Error parsing KCL source after adding constraint: {errors:?}"
4652 ))));
4653 }
4654 let Some(new_program) = new_program else {
4655 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
4656 "No AST produced after adding constraint".to_string(),
4657 )));
4658 };
4659 let constraint_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
4660 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
4661 "Source range of new constraint not found in sketch block: {sketch_block_ref:?}; {err:?}"
4662 )))
4663 })?;
4664
4665 let mut truncated_program = new_program.clone();
4668 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
4669 .map_err(KclErrorWithOutputs::no_outputs)?;
4670
4671 let outcome = ctx
4673 .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch_id))
4674 .await?;
4675
4676 let new_object_ids = {
4677 let constraint_id = outcome
4679 .source_range_to_object
4680 .get(&constraint_node_ref.range)
4681 .copied()
4682 .ok_or_else(|| {
4683 KclErrorWithOutputs::from_error_outcome(
4684 KclError::refactor(format!("Source range of constraint not found: {constraint_node_ref:?}")),
4685 outcome.clone(),
4686 )
4687 })?;
4688 vec![constraint_id]
4689 };
4690
4691 self.program = new_program;
4694
4695 let outcome = self.update_state_after_exec(outcome, true);
4697
4698 let src_delta = self.commit_var_solutions_to_program(&outcome, "adding constraint")?;
4699 let scene_graph_delta = SceneGraphDelta {
4700 new_graph: self.scene_graph_for_ui(),
4701 invalidates_ids: false,
4702 new_objects: new_object_ids,
4703 exec_outcome: outcome,
4704 };
4705 Ok((src_delta, scene_graph_delta))
4706 }
4707
4708 fn commit_var_solutions_to_program(&mut self, outcome: &ExecOutcome, operation: &str) -> ExecResult<SourceDelta> {
4709 let commit_failure = || {
4710 KclErrorWithOutputs::from_error_outcome(
4711 KclError::refactor(format!("Could not update KCL after {operation}.")),
4712 outcome.clone(),
4713 )
4714 };
4715
4716 let default_length_unit = self.default_length_unit();
4717 let mut settled_ast = self.program.ast.clone();
4718 let mut committed_solver_value = false;
4719 for (var_range, node_path, value) in &outcome.var_solutions {
4720 let Some(lookup) = numeric_literal_at_node_path(&settled_ast, node_path.as_ref(), *var_range) else {
4721 return Err(commit_failure());
4722 };
4723 let new_value = match &lookup {
4724 Some(current_literal) => {
4725 if !var_solution_needs_commit(current_literal, *value, default_length_unit) {
4726 continue;
4727 }
4728 preserve_var_solution_literal_style(current_literal, *value, default_length_unit)
4729 }
4730 None => {
4731 Number {
4735 value: number_value_in_default_length_units(*value, default_length_unit),
4736 units: default_length_unit.into(),
4737 }
4738 }
4739 };
4740 committed_solver_value = true;
4741 let source_ref = SourceRef::Simple {
4742 range: *var_range,
4743 node_path: node_path.clone(),
4744 };
4745 mutate_ast_node_by_source_ref(
4746 &mut settled_ast,
4747 &source_ref,
4748 AstMutateCommand::EditVarInitialValue { value: new_value },
4749 )
4750 .map_err(|_| commit_failure())?;
4751 }
4752
4753 if !committed_solver_value {
4754 return Ok(SourceDelta {
4755 text: self.program.original_file_contents.clone(),
4756 });
4757 }
4758
4759 let settled_source = source_from_ast(&settled_ast);
4760 let (settled_program, errors) = Program::parse(&settled_source).map_err(|_| commit_failure())?;
4761 if !errors.is_empty() {
4762 return Err(commit_failure());
4763 }
4764 let Some(settled_program) = settled_program else {
4765 return Err(commit_failure());
4766 };
4767
4768 self.program = settled_program;
4769
4770 Ok(SourceDelta { text: settled_source })
4771 }
4772
4773 fn segment_will_be_deleted(&self, segment_id: ObjectId, segment_ids_set: &AhashIndexSet<ObjectId>) -> bool {
4775 if segment_ids_set.contains(&segment_id) {
4776 return true;
4777 }
4778
4779 let Some(segment_object) = self.scene_graph.objects.get(segment_id.0) else {
4780 return false;
4781 };
4782 let ObjectKind::Segment { segment } = &segment_object.kind else {
4783 return false;
4784 };
4785 let Segment::Point(point) = segment else {
4786 return false;
4787 };
4788
4789 point.owner.is_some_and(|owner_id| segment_ids_set.contains(&owner_id))
4790 }
4791
4792 fn remaining_constraint_segments(
4793 &self,
4794 segments: &[ConstraintSegment],
4795 segment_ids_set: &AhashIndexSet<ObjectId>,
4796 ) -> Vec<ConstraintSegment> {
4797 segments
4798 .iter()
4799 .copied()
4800 .filter(|segment| match segment {
4801 ConstraintSegment::Origin(_) => true,
4802 ConstraintSegment::Segment(segment_id) => !self.segment_will_be_deleted(*segment_id, segment_ids_set),
4803 })
4804 .collect()
4805 }
4806
4807 fn find_referenced_constraints(
4808 &self,
4809 sketch_id: ObjectId,
4810 segment_ids_set: &AhashIndexSet<ObjectId>,
4811 ) -> Result<AhashIndexSet<ObjectId>, KclError> {
4812 let sketch_object = self
4814 .scene_graph
4815 .objects
4816 .get(sketch_id.0)
4817 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch_id:?}")))?;
4818 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
4819 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
4820 };
4821 let segment_or_owner_matches = |segment_id: ObjectId| {
4822 if segment_ids_set.contains(&segment_id) {
4823 return true;
4824 }
4825 let segment_object = self.scene_graph.objects.get(segment_id.0);
4826 if let Some(obj) = segment_object
4827 && let ObjectKind::Segment { segment } = &obj.kind
4828 {
4829 match segment {
4830 Segment::Point(point) => point.owner.is_some_and(|owner_id| segment_ids_set.contains(&owner_id)),
4831 Segment::Line(line) => line.owner.is_some_and(|owner_id| segment_ids_set.contains(&owner_id)),
4832 _ => false,
4833 }
4834 } else {
4835 false
4836 }
4837 };
4838 let mut constraint_ids_set = AhashIndexSet::default();
4839 for constraint_id in &sketch.constraints {
4840 let constraint_object = self
4841 .scene_graph
4842 .objects
4843 .get(constraint_id.0)
4844 .ok_or_else(|| KclError::refactor(format!("Constraint not found: {constraint_id:?}")))?;
4845 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
4846 return Err(KclError::refactor(format!(
4847 "Object is not a constraint, it is {}",
4848 constraint_object.kind.human_friendly_kind_with_article()
4849 )));
4850 };
4851 let depends_on_segment = match constraint {
4852 Constraint::Coincident(c) => c.segment_ids().any(segment_or_owner_matches),
4853 Constraint::Distance(d) => d.point_ids().any(segment_or_owner_matches),
4854 Constraint::Fixed(fixed) => fixed
4855 .points
4856 .iter()
4857 .any(|fixed_point| self.segment_will_be_deleted(fixed_point.point, segment_ids_set)),
4858 Constraint::Radius(r) => segment_or_owner_matches(r.arc),
4859 Constraint::Diameter(d) => segment_or_owner_matches(d.arc),
4860 Constraint::EqualRadius(equal_radius) => {
4861 equal_radius.input.iter().copied().any(segment_or_owner_matches)
4862 }
4863 Constraint::HorizontalDistance(d) => d.point_ids().any(segment_or_owner_matches),
4864 Constraint::VerticalDistance(d) => d.point_ids().any(segment_or_owner_matches),
4865 Constraint::Horizontal(h) => match h {
4866 Horizontal::Line { line } => segment_or_owner_matches(*line),
4867 Horizontal::Points { points } => points.iter().any(|point| match point {
4868 ConstraintSegment::Segment(point) => segment_or_owner_matches(*point),
4869 ConstraintSegment::Origin(_) => false,
4870 }),
4871 },
4872 Constraint::Vertical(v) => match v {
4873 Vertical::Line { line } => segment_or_owner_matches(*line),
4874 Vertical::Points { points } => points.iter().any(|point| match point {
4875 ConstraintSegment::Segment(point) => segment_or_owner_matches(*point),
4876 ConstraintSegment::Origin(_) => false,
4877 }),
4878 },
4879 Constraint::LinesEqualLength(lines_equal_length) => {
4880 lines_equal_length.lines.iter().copied().any(segment_or_owner_matches)
4881 }
4882 Constraint::Midpoint(midpoint) => {
4883 segment_or_owner_matches(midpoint.segment)
4884 || matches!(
4885 midpoint.point,
4886 ConstraintSegment::Segment(point) if segment_or_owner_matches(point)
4887 )
4888 }
4889 Constraint::Parallel(parallel) => parallel.lines.iter().copied().any(segment_or_owner_matches),
4890 Constraint::Perpendicular(perpendicular) => {
4891 perpendicular.lines.iter().copied().any(segment_or_owner_matches)
4892 }
4893 Constraint::Angle(angle) => angle.lines.iter().copied().any(segment_or_owner_matches),
4894 Constraint::Symmetric(symmetric) => {
4895 segment_or_owner_matches(symmetric.axis)
4896 || symmetric.input.iter().copied().any(segment_or_owner_matches)
4897 }
4898 Constraint::Tangent(tangent) => tangent.input.iter().copied().any(segment_or_owner_matches),
4899 };
4900 if depends_on_segment {
4901 constraint_ids_set.insert(*constraint_id);
4902 }
4903 }
4904 Ok(constraint_ids_set)
4905 }
4906
4907 fn update_state_after_exec(&mut self, outcome: ExecOutcome, freedom_analysis_ran: bool) -> ExecOutcome {
4908 let mut outcome = outcome;
4909 let mut new_objects = std::mem::take(&mut outcome.scene_objects);
4910
4911 if freedom_analysis_ran {
4912 self.point_freedom_cache.clear();
4915 for new_obj in &new_objects {
4916 if let ObjectKind::Segment {
4917 segment: crate::front::Segment::Point(point),
4918 } = &new_obj.kind
4919 {
4920 self.point_freedom_cache.insert(new_obj.id, point.freedom);
4921 }
4922 }
4923 add_wall_and_cap_face_objects(&mut new_objects, &outcome.artifact_graph);
4924 self.scene_graph.objects = new_objects;
4926 } else {
4927 for old_obj in &self.scene_graph.objects {
4930 if let ObjectKind::Segment {
4931 segment: crate::front::Segment::Point(point),
4932 } = &old_obj.kind
4933 {
4934 self.point_freedom_cache.insert(old_obj.id, point.freedom);
4935 }
4936 }
4937
4938 let mut updated_objects = Vec::with_capacity(new_objects.len());
4940 for new_obj in new_objects {
4941 let mut obj = new_obj;
4942 if let ObjectKind::Segment {
4943 segment: crate::front::Segment::Point(point),
4944 } = &mut obj.kind
4945 {
4946 let new_freedom = point.freedom;
4947 match new_freedom {
4953 Freedom::Free => {
4954 match self.point_freedom_cache.get(&obj.id).copied() {
4955 Some(Freedom::Conflict) => {
4956 }
4959 Some(Freedom::Fixed) => {
4960 point.freedom = Freedom::Fixed;
4962 }
4963 Some(Freedom::Free) => {
4964 }
4966 None => {
4967 }
4969 }
4970 }
4971 Freedom::Fixed => {
4972 }
4974 Freedom::Conflict => {
4975 }
4977 }
4978 self.point_freedom_cache.insert(obj.id, point.freedom);
4980 }
4981 updated_objects.push(obj);
4982 }
4983
4984 add_wall_and_cap_face_objects(&mut updated_objects, &outcome.artifact_graph);
4985 self.scene_graph.objects = updated_objects;
4986 }
4987 outcome
4988 }
4989
4990 fn mutate_ast(
4991 &mut self,
4992 ast: &mut ast::Node<ast::Program>,
4993 object_id: ObjectId,
4994 command: AstMutateCommand,
4995 ) -> Result<(AstNodeRef, AstMutateCommandReturn), KclError> {
4996 let sketch_object = self
4997 .scene_graph
4998 .objects
4999 .get(object_id.0)
5000 .ok_or_else(|| KclError::refactor(format!("Object not found: {object_id:?}")))?;
5001 mutate_ast_node_by_source_ref(ast, &sketch_object.source, command)
5002 }
5003}
5004
5005fn sketch_block_ref_from_id(scene_graph: &SceneGraph, sketch_id: ObjectId) -> Result<AstNodeRef, KclError> {
5006 let sketch_object = scene_graph
5008 .objects
5009 .get(sketch_id.0)
5010 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch_id:?}")))?;
5011 let ObjectKind::Sketch(_) = &sketch_object.kind else {
5012 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
5013 };
5014 expect_single_node_ref(sketch_object)
5015}
5016
5017fn expect_single_node_ref(object: &Object) -> Result<AstNodeRef, KclError> {
5018 match &object.source {
5019 SourceRef::Simple { range, node_path } => Ok(AstNodeRef {
5020 range: *range,
5021 node_path: node_path.clone(),
5022 }),
5023 SourceRef::BackTrace { ranges } => {
5024 let [range] = ranges.as_slice() else {
5025 return Err(KclError::refactor(format!(
5026 "Expected single location in SourceRef, got {}; ranges={ranges:#?}",
5027 ranges.len()
5028 )));
5029 };
5030 Ok(AstNodeRef {
5031 range: range.0,
5032 node_path: range.1.clone(),
5033 })
5034 }
5035 }
5036}
5037
5038fn only_sketch_block_from_range(
5041 ast: &mut ast::Node<ast::Program>,
5042 sketch_block_range: SourceRange,
5043 edit_kind: ChangeKind,
5044) -> Result<(), KclError> {
5045 let r1 = sketch_block_range;
5046 let matches_range = |r2: SourceRange| -> bool {
5047 match edit_kind {
5050 ChangeKind::Add => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() <= r2.end(),
5051 ChangeKind::Edit => r1.module_id() == r2.module_id() && r1.start() == r2.start(),
5053 ChangeKind::Delete => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() >= r2.end(),
5054 ChangeKind::None => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() == r2.end(),
5056 }
5057 };
5058 let mut found = false;
5059 for item in ast.body.iter_mut() {
5060 match item {
5061 ast::BodyItem::ImportStatement(_) => {}
5062 ast::BodyItem::ExpressionStatement(node) => {
5063 if matches_range(SourceRange::from(&*node))
5064 && let ast::Expr::SketchBlock(sketch_block) = &mut node.expression
5065 {
5066 sketch_block.is_being_edited = true;
5067 found = true;
5068 break;
5069 }
5070 }
5071 ast::BodyItem::VariableDeclaration(node) => {
5072 if matches_range(SourceRange::from(&node.declaration.init))
5073 && let ast::Expr::SketchBlock(sketch_block) = &mut node.declaration.init
5074 {
5075 sketch_block.is_being_edited = true;
5076 found = true;
5077 break;
5078 }
5079 }
5080 ast::BodyItem::TypeDeclaration(_) => {}
5081 ast::BodyItem::ReturnStatement(node) => {
5082 if matches_range(SourceRange::from(&node.argument))
5083 && let ast::Expr::SketchBlock(sketch_block) = &mut node.argument
5084 {
5085 sketch_block.is_being_edited = true;
5086 found = true;
5087 break;
5088 }
5089 }
5090 }
5091 }
5092 if !found {
5093 return Err(KclError::refactor(format!(
5094 "Sketch block source range not found in AST: {sketch_block_range:?}, edit_kind={edit_kind:?}"
5095 )));
5096 }
5097
5098 Ok(())
5099}
5100
5101fn only_sketch_block(
5102 ast: &mut ast::Node<ast::Program>,
5103 sketch_block_ref: &AstNodeRef,
5104 edit_kind: ChangeKind,
5105) -> Result<(), KclError> {
5106 let Some(target_node_path) = &sketch_block_ref.node_path else {
5107 #[cfg(target_arch = "wasm32")]
5108 web_sys::console::warn_1(
5109 &format!(
5110 "only_sketch_block: target sketch block ref doesn't have node path; sketch_block_ref={:#?}, edit_kind={edit_kind:#?}",
5111 &sketch_block_ref
5112 )
5113 .into(),
5114 );
5115 return only_sketch_block_from_range(ast, sketch_block_ref.range, edit_kind);
5116 };
5117 let mut found = false;
5118 for item in ast.body.iter_mut() {
5119 match item {
5120 ast::BodyItem::ImportStatement(_) => {}
5121 ast::BodyItem::ExpressionStatement(node) => {
5122 if let Some(node_path) = &node.node_path
5124 && node_path == target_node_path
5125 && let ast::Expr::SketchBlock(sketch_block) = &mut node.expression
5126 {
5127 sketch_block.is_being_edited = true;
5128 found = true;
5129 break;
5130 }
5131 if let Some(node_path) = node.expression.node_path()
5133 && node_path == target_node_path
5134 && let ast::Expr::SketchBlock(sketch_block) = &mut node.expression
5135 {
5136 sketch_block.is_being_edited = true;
5137 found = true;
5138 break;
5139 }
5140 }
5141 ast::BodyItem::VariableDeclaration(node) => {
5142 if let Some(node_path) = node.declaration.init.node_path()
5143 && node_path == target_node_path
5144 && let ast::Expr::SketchBlock(sketch_block) = &mut node.declaration.init
5145 {
5146 sketch_block.is_being_edited = true;
5147 found = true;
5148 break;
5149 }
5150 }
5151 ast::BodyItem::TypeDeclaration(_) => {}
5152 ast::BodyItem::ReturnStatement(node) => {
5153 if let Some(node_path) = node.argument.node_path()
5154 && node_path == target_node_path
5155 && let ast::Expr::SketchBlock(sketch_block) = &mut node.argument
5156 {
5157 sketch_block.is_being_edited = true;
5158 found = true;
5159 break;
5160 }
5161 }
5162 }
5163 }
5164 if !found {
5165 return Err(KclError::refactor(format!(
5166 "Sketch block node path not found in AST: {sketch_block_ref:?}, edit_kind={edit_kind:?}"
5167 )));
5168 }
5169
5170 Ok(())
5171}
5172
5173fn sketch_on_ast_expr(
5174 ast: &mut ast::Node<ast::Program>,
5175 scene_graph: &SceneGraph,
5176 on: &Plane,
5177) -> Result<ast::Expr, KclError> {
5178 match on {
5179 Plane::Default(name) => Ok(default_plane_ast_expr(*name)),
5180 Plane::Object(object_id) => {
5181 let on_object = scene_graph
5182 .objects
5183 .get(object_id.0)
5184 .ok_or_else(|| KclError::refactor(format!("Sketch plane object not found: {object_id:?}")))?;
5185 if let Some(face_expr) = sketch_face_of_scene_object_ast_expr(ast, on_object)? {
5186 return Ok(face_expr);
5187 }
5188 get_or_insert_ast_reference(ast, &on_object.source, "plane", None)
5189 }
5190 }
5191}
5192
5193fn sketch_face_of_scene_object_ast_expr(
5194 ast: &mut ast::Node<ast::Program>,
5195 on_object: &crate::front::Object,
5196) -> Result<Option<ast::Expr>, KclError> {
5197 match &on_object.kind {
5198 ObjectKind::Wall(wall) => {
5199 let solid_ref = get_or_insert_ast_reference(
5200 ast,
5201 &source_ref_from_source_ref_range(&wall.source.solid),
5202 "solid",
5203 None,
5204 )?;
5205 let ast::Expr::Name(solid_name_expr) = solid_ref else {
5206 return Err(KclError::refactor(format!(
5207 "Could not resolve solid reference for selected wall: artifact_id={:?}",
5208 on_object.artifact_id
5209 )));
5210 };
5211 let solid_expr = indexed_solid_expr_for_sweep_output(
5212 ast_name_expr(solid_name_expr.name.name.clone()),
5213 wall.solid_output_index,
5214 );
5215 let sweep_ref = get_or_insert_ast_reference(
5216 ast,
5217 &source_ref_from_source_ref_range(&wall.source.sweep),
5218 "solid",
5219 None,
5220 )?;
5221 let ast::Expr::Name(sweep_name_expr) = sweep_ref else {
5222 return Err(KclError::refactor(format!(
5223 "Could not resolve sweep reference for selected wall: artifact_id={:?}",
5224 on_object.artifact_id
5225 )));
5226 };
5227 let sweep_name = sweep_name_expr.name.name.clone();
5228 let segment_ref = get_or_insert_ast_reference(
5229 ast,
5230 &source_ref_from_source_ref_range(&wall.source.segment),
5231 LINE_VARIABLE,
5232 None,
5233 )?;
5234
5235 let face_expr = if let Some(region_name) = region_name_from_sweep_variable(ast, &sweep_name).or_else(|| {
5236 wall.source
5237 .path
5238 .as_ref()
5239 .and_then(|path_source| region_name_from_path_source(ast, path_source))
5240 }) {
5241 let ast::Expr::Name(segment_name_expr) = segment_ref else {
5242 return Err(KclError::refactor(format!(
5243 "Could not resolve source segment reference for selected region wall: artifact_id={:?}",
5244 on_object.artifact_id
5245 )));
5246 };
5247 create_member_expression(
5248 create_member_expression(ast_name_expr(region_name), "tags"),
5249 &segment_name_expr.name.name,
5250 )
5251 } else {
5252 segment_ref
5253 };
5254
5255 Ok(Some(create_face_of_ast(solid_expr, face_expr)))
5256 }
5257 ObjectKind::Cap(cap) => {
5258 let solid_ref =
5259 get_or_insert_ast_reference(ast, &source_ref_from_source_ref_range(&cap.source.solid), "solid", None)?;
5260 let ast::Expr::Name(solid_name_expr) = solid_ref else {
5261 return Err(KclError::refactor(format!(
5262 "Could not resolve solid reference for selected cap: artifact_id={:?}",
5263 on_object.artifact_id
5264 )));
5265 };
5266 let solid_expr = indexed_solid_expr_for_sweep_output(
5267 ast_name_expr(solid_name_expr.name.name.clone()),
5268 cap.solid_output_index,
5269 );
5270 let face_expr = match cap.kind {
5272 crate::frontend::api::CapKind::Start => ast_name_expr("START".to_owned()),
5273 crate::frontend::api::CapKind::End => ast_name_expr("END".to_owned()),
5274 };
5275
5276 Ok(Some(create_face_of_ast(solid_expr, face_expr)))
5277 }
5278 _ => Ok(None),
5279 }
5280}
5281
5282fn indexed_solid_expr_for_sweep_output(solid_expr: ast::Expr, solid_output_index: Option<usize>) -> ast::Expr {
5283 match solid_output_index {
5284 Some(output_index) => create_index_expression(solid_expr, output_index),
5285 None => solid_expr,
5286 }
5287}
5288
5289fn source_ref_from_source_ref_range(source: &SourceRefRange) -> SourceRef {
5290 SourceRef::Simple {
5291 range: source.range,
5292 node_path: source.node_path.clone(),
5293 }
5294}
5295
5296fn region_name_from_path_source(ast: &ast::Node<ast::Program>, path_source: &SourceRefRange) -> Option<String> {
5297 let source_ref = source_ref_from_source_ref_range(path_source);
5298 let candidate = variable_name_containing_source_ref(ast, &source_ref)?;
5299 let ast::Definition::Variable(region_decl) = ast.get_variable(&candidate)? else {
5300 return None;
5301 };
5302 let ast::Expr::CallExpressionKw(region_call) = ®ion_decl.init else {
5303 return None;
5304 };
5305 if region_call.callee.name.name != "region" {
5306 return None;
5307 }
5308 Some(candidate)
5309}
5310
5311fn downstream_composite_code_ref_for_source(artifact_graph: &ArtifactGraph, source_id: ArtifactId) -> Option<&CodeRef> {
5312 let mut current_id = source_id;
5313 let mut current_composite = None;
5314 let mut visited = HashSet::new();
5315
5316 while visited.insert(current_id) {
5317 let next_composite_id = downstream_composite_id_for_solid_source(artifact_graph, current_id);
5318
5319 let Some(composite_id) = next_composite_id else {
5320 break;
5321 };
5322 let Some(Artifact::CompositeSolid(composite)) = artifact_graph.get(&composite_id) else {
5323 break;
5324 };
5325
5326 current_id = composite.id;
5327 current_composite = Some(composite);
5328
5329 if !composite.consumed {
5330 break;
5331 }
5332 }
5333
5334 current_composite.map(|composite| &composite.code_ref)
5335}
5336
5337fn downstream_composite_id_for_solid_source(
5338 artifact_graph: &ArtifactGraph,
5339 source_id: ArtifactId,
5340) -> Option<ArtifactId> {
5341 if let Some(Artifact::Path(path)) = artifact_graph.get(&source_id)
5343 && let Some(composite_id) = path.composite_solid_id
5344 && let Some(Artifact::CompositeSolid(composite)) = artifact_graph.get(&composite_id)
5345 && composite_contains_path_input(&composite.solid_ids, &composite.tool_ids, path.id, path.solid2d_id)
5346 {
5347 return Some(composite_id);
5348 }
5349
5350 for artifact in artifact_graph.values() {
5352 if let Artifact::Path(path) = artifact
5353 && path.sweep_id == Some(source_id)
5354 && let Some(composite_id) = path.composite_solid_id
5355 && let Some(Artifact::CompositeSolid(composite)) = artifact_graph.get(&composite_id)
5356 && composite_contains_path_input(&composite.solid_ids, &composite.tool_ids, path.id, path.solid2d_id)
5357 {
5358 return Some(composite_id);
5359 }
5360 }
5361
5362 artifact_graph.values().find_map(|artifact| {
5364 let Artifact::CompositeSolid(composite) = artifact else {
5365 return None;
5366 };
5367 composite_contains_input(&composite.solid_ids, &composite.tool_ids, source_id).then_some(composite.id)
5368 })
5369}
5370
5371fn composite_contains_path_input(
5372 solid_ids: &[ArtifactId],
5373 tool_ids: &[ArtifactId],
5374 path_id: ArtifactId,
5375 solid2d_id: Option<ArtifactId>,
5376) -> bool {
5377 composite_contains_input(solid_ids, tool_ids, path_id)
5378 || solid2d_id.is_some_and(|solid2d_id| composite_contains_input(solid_ids, tool_ids, solid2d_id))
5379}
5380
5381fn composite_contains_input(solid_ids: &[ArtifactId], tool_ids: &[ArtifactId], input_id: ArtifactId) -> bool {
5382 solid_ids.contains(&input_id) || tool_ids.contains(&input_id)
5383}
5384
5385fn code_ref_source_ref_range(code_ref: &CodeRef) -> SourceRefRange {
5386 let node_path = (!code_ref.node_path.is_empty()).then(|| code_ref.node_path.clone());
5387 SourceRefRange {
5388 range: code_ref.range,
5389 node_path,
5390 }
5391}
5392
5393fn solid_output_index_for_sweep(
5394 artifact_graph: &ArtifactGraph,
5395 sweep_id: ArtifactId,
5396 sweep_code_ref: &CodeRef,
5397) -> Option<usize> {
5398 let sibling_sweeps = artifact_graph
5399 .values()
5400 .filter_map(|artifact| match artifact {
5401 Artifact::Sweep(sweep)
5402 if sweep.code_ref.range == sweep_code_ref.range
5403 && sweep.code_ref.node_path == sweep_code_ref.node_path =>
5404 {
5405 Some(sweep)
5406 }
5407 _ => None,
5408 })
5409 .collect::<Vec<_>>();
5410
5411 if sibling_sweeps.len() <= 1 {
5412 return None;
5413 }
5414
5415 sibling_sweeps
5416 .iter()
5417 .position(|sibling_sweep| sibling_sweep.id == sweep_id)
5418}
5419
5420fn add_wall_and_cap_face_objects(scene_objects: &mut Vec<crate::front::Object>, artifact_graph: &ArtifactGraph) {
5421 let mut existing_artifact_ids = scene_objects
5422 .iter()
5423 .map(|object| object.artifact_id)
5424 .collect::<HashSet<_>>();
5425
5426 for artifact in artifact_graph.values() {
5427 match artifact {
5428 Artifact::Wall(wall) => {
5429 if existing_artifact_ids.contains(&wall.id) {
5430 continue;
5431 }
5432
5433 let Some(segment) = artifact_graph.get(&wall.seg_id).and_then(|artifact| match artifact {
5434 Artifact::Segment(segment) => Some(segment),
5435 _ => None,
5436 }) else {
5437 continue;
5438 };
5439 let Some(sweep) = artifact_graph.get(&wall.sweep_id).and_then(|artifact| match artifact {
5440 Artifact::Sweep(sweep) => Some(sweep),
5441 _ => None,
5442 }) else {
5443 continue;
5444 };
5445 let source_segment = segment
5446 .original_seg_id
5447 .and_then(|original_seg_id| artifact_graph.get(&original_seg_id))
5448 .and_then(|artifact| match artifact {
5449 Artifact::Segment(segment) => Some(segment),
5450 _ => None,
5451 })
5452 .unwrap_or(segment);
5453 let solid_code_ref =
5454 downstream_composite_code_ref_for_source(artifact_graph, wall.sweep_id).unwrap_or(&sweep.code_ref);
5455 let path_code_ref = artifact_graph
5456 .get(&segment.path_id)
5457 .or_else(|| artifact_graph.get(&sweep.path_id))
5458 .and_then(|artifact| match artifact {
5459 Artifact::Path(path) => Some(&path.code_ref),
5460 _ => None,
5461 });
5462 let source = WallSource {
5463 solid: code_ref_source_ref_range(solid_code_ref),
5464 sweep: code_ref_source_ref_range(&sweep.code_ref),
5465 path: path_code_ref.map(code_ref_source_ref_range),
5466 segment: code_ref_source_ref_range(&source_segment.code_ref),
5467 };
5468 let solid_output_index = (solid_code_ref.range == sweep.code_ref.range
5469 && solid_code_ref.node_path == sweep.code_ref.node_path)
5470 .then(|| solid_output_index_for_sweep(artifact_graph, sweep.id, &sweep.code_ref))
5471 .flatten();
5472 let object_source = source_ref_from_source_ref_range(&source.solid);
5473 let id = ObjectId(scene_objects.len());
5474 scene_objects.push(crate::front::Object {
5475 id,
5476 kind: ObjectKind::Wall(crate::frontend::api::Wall {
5477 id,
5478 source,
5479 solid_output_index,
5480 }),
5481 label: Default::default(),
5482 comments: Default::default(),
5483 artifact_id: wall.id,
5484 source: object_source,
5485 });
5486 existing_artifact_ids.insert(wall.id);
5487 }
5488 Artifact::Cap(cap) => {
5489 if existing_artifact_ids.contains(&cap.id) {
5490 continue;
5491 }
5492
5493 let Some(sweep) = artifact_graph.get(&cap.sweep_id).and_then(|artifact| match artifact {
5494 Artifact::Sweep(sweep) => Some(sweep),
5495 _ => None,
5496 }) else {
5497 continue;
5498 };
5499 let id = ObjectId(scene_objects.len());
5500 let kind = match cap.sub_type {
5501 CapSubType::Start => crate::frontend::api::CapKind::Start,
5502 CapSubType::End => crate::frontend::api::CapKind::End,
5503 };
5504 let solid_code_ref =
5505 downstream_composite_code_ref_for_source(artifact_graph, cap.sweep_id).unwrap_or(&sweep.code_ref);
5506 let source = CapSource {
5507 solid: code_ref_source_ref_range(solid_code_ref),
5508 sweep: code_ref_source_ref_range(&sweep.code_ref),
5509 };
5510 let solid_output_index = (solid_code_ref.range == sweep.code_ref.range
5511 && solid_code_ref.node_path == sweep.code_ref.node_path)
5512 .then(|| solid_output_index_for_sweep(artifact_graph, sweep.id, &sweep.code_ref))
5513 .flatten();
5514 let object_source = source_ref_from_source_ref_range(&source.solid);
5515 scene_objects.push(crate::front::Object {
5516 id,
5517 kind: ObjectKind::Cap(crate::frontend::api::Cap {
5518 id,
5519 kind,
5520 source,
5521 solid_output_index,
5522 }),
5523 label: Default::default(),
5524 comments: Default::default(),
5525 artifact_id: cap.id,
5526 source: object_source,
5527 });
5528 existing_artifact_ids.insert(cap.id);
5529 }
5530 _ => {}
5531 }
5532 }
5533}
5534
5535fn default_plane_ast_expr(name: crate::engine::PlaneName) -> ast::Expr {
5536 use crate::engine::PlaneName;
5537
5538 match name {
5539 PlaneName::Xy => ast_name_expr("XY".to_owned()),
5540 PlaneName::Xz => ast_name_expr("XZ".to_owned()),
5541 PlaneName::Yz => ast_name_expr("YZ".to_owned()),
5542 PlaneName::NegXy => negated_plane_ast_expr("XY"),
5543 PlaneName::NegXz => negated_plane_ast_expr("XZ"),
5544 PlaneName::NegYz => negated_plane_ast_expr("YZ"),
5545 }
5546}
5547
5548fn negated_plane_ast_expr(name: &str) -> ast::Expr {
5549 ast::Expr::UnaryExpression(Box::new(ast::UnaryExpression::new(
5550 ast::UnaryOperator::Neg,
5551 ast::BinaryPart::Name(Box::new(ast_name(name.to_owned()))),
5552 )))
5553}
5554
5555fn create_face_of_ast(solid_expr: ast::Expr, face_expr: ast::Expr) -> ast::Expr {
5556 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5557 callee: ast::Node::no_src(ast_sketch2_name("faceOf")),
5558 unlabeled: Some(solid_expr),
5559 arguments: vec![ast::LabeledArg {
5560 label: Some(ast::Identifier::new("face")),
5561 arg: face_expr,
5562 }],
5563 digest: None,
5564 non_code_meta: Default::default(),
5565 })))
5566}
5567
5568fn region_name_from_sweep_variable(ast: &ast::Node<ast::Program>, sweep_variable_name: &str) -> Option<String> {
5569 let ast::Definition::Variable(sweep_decl) = ast.get_variable(sweep_variable_name)? else {
5570 return None;
5571 };
5572 let ast::Expr::CallExpressionKw(sweep_call) = &sweep_decl.init else {
5573 return None;
5574 };
5575 if !matches!(
5576 sweep_call.callee.name.name.as_str(),
5577 "extrude" | "revolve" | "sweep" | "loft"
5578 ) {
5579 return None;
5580 }
5581 let ast::Expr::Name(region_name_expr) = sweep_call.unlabeled.as_ref()? else {
5582 return None;
5583 };
5584 let candidate = region_name_expr.name.name.clone();
5585 let ast::Definition::Variable(region_decl) = ast.get_variable(&candidate)? else {
5586 return None;
5587 };
5588 let ast::Expr::CallExpressionKw(region_call) = ®ion_decl.init else {
5589 return None;
5590 };
5591 if region_call.callee.name.name != "region" {
5592 return None;
5593 }
5594 Some(candidate)
5595}
5596
5597fn get_or_insert_ast_reference(
5604 ast: &mut ast::Node<ast::Program>,
5605 source_ref: &SourceRef,
5606 prefix: &str,
5607 property: Option<&str>,
5608) -> Result<ast::Expr, KclError> {
5609 let command = AstMutateCommand::AddVariableDeclaration {
5610 prefix: prefix.to_owned(),
5611 };
5612 let ret = match mutate_ast_node_by_source_ref(ast, source_ref, command) {
5613 Ok((_, ret)) => ret,
5614 Err(err) => {
5615 if let Some(var_name) = variable_name_containing_source_ref(ast, source_ref) {
5616 AstMutateCommandReturn::Name(var_name)
5617 } else {
5618 return Err(err);
5619 }
5620 }
5621 };
5622 let AstMutateCommandReturn::Name(var_name) = ret else {
5623 return Err(KclError::refactor(
5624 "Expected variable name returned from AddVariableDeclaration".to_owned(),
5625 ));
5626 };
5627 let var_expr = ast::Expr::Name(Box::new(ast::Name::new(&var_name)));
5628 let Some(property) = property else {
5629 return Ok(var_expr);
5631 };
5632
5633 Ok(create_member_expression(var_expr, property))
5634}
5635
5636fn variable_name_containing_source_ref(ast: &ast::Node<ast::Program>, source_ref: &SourceRef) -> Option<String> {
5637 let source_range = match source_ref {
5638 SourceRef::Simple { range, .. } => *range,
5639 SourceRef::BackTrace { ranges } => {
5640 let [range] = ranges.as_slice() else {
5641 return None;
5642 };
5643 range.0
5644 }
5645 };
5646 ast.body.iter().find_map(|item| {
5647 let ast::BodyItem::VariableDeclaration(var_decl) = item else {
5648 return None;
5649 };
5650 let init_range = SourceRange::from(&var_decl.declaration.init);
5651 let source_is_inside_init = init_range.module_id() == source_range.module_id()
5652 && init_range.start() <= source_range.start()
5653 && source_range.end() <= init_range.end();
5654 if matches!(&var_decl.declaration.init, ast::Expr::SketchBlock(_))
5655 && init_range != source_range
5656 && source_is_inside_init
5657 {
5658 return None;
5659 }
5660 source_is_inside_init.then(|| var_decl.name().to_owned())
5661 })
5662}
5663
5664fn mutate_ast_node_by_source_ref(
5665 ast: &mut ast::Node<ast::Program>,
5666 source_ref: &SourceRef,
5667 command: AstMutateCommand,
5668) -> Result<(AstNodeRef, AstMutateCommandReturn), KclError> {
5669 let (source_range, node_path) = match source_ref {
5670 SourceRef::Simple { range, node_path } => (*range, node_path.clone()),
5671 SourceRef::BackTrace { ranges } => {
5672 let [range] = ranges.as_slice() else {
5673 return Err(KclError::refactor(format!(
5674 "Expected single source ref, got {}; ranges={ranges:#?}",
5675 ranges.len(),
5676 )));
5677 };
5678 (range.0, range.1.clone())
5679 }
5680 };
5681 let mut context = AstMutateContext {
5682 source_range,
5683 node_path,
5684 command,
5685 defined_names_stack: Default::default(),
5686 };
5687 let control = dfs_mut(ast, &mut context);
5688 match control {
5689 ControlFlow::Continue(_) => Err(KclError::refactor(
5690 "Could not find the KCL source for this edit. Try reloading the app, or update from code.".to_owned(),
5691 )),
5692 ControlFlow::Break(break_value) => break_value,
5693 }
5694}
5695
5696#[derive(Debug)]
5697struct AstMutateContext {
5698 source_range: SourceRange,
5699 node_path: Option<ast::NodePath>,
5700 command: AstMutateCommand,
5701 defined_names_stack: Vec<HashSet<String>>,
5702}
5703
5704#[derive(Debug)]
5705#[allow(clippy::large_enum_variant)]
5706enum AstMutateCommand {
5707 AddSketchBlockExprStmt {
5709 expr: ast::Expr,
5710 },
5711 AddSketchBlockVarDecl {
5713 prefix: String,
5714 expr: ast::Expr,
5715 },
5716 AddVariableDeclaration {
5717 prefix: String,
5718 },
5719 EditPoint {
5720 at: ast::Expr,
5721 },
5722 EditLine {
5723 start: ast::Expr,
5724 end: ast::Expr,
5725 construction: Option<bool>,
5726 },
5727 EditArc {
5728 start: ast::Expr,
5729 end: ast::Expr,
5730 center: ast::Expr,
5731 construction: Option<bool>,
5732 },
5733 EditCircle {
5734 start: ast::Expr,
5735 center: ast::Expr,
5736 construction: Option<bool>,
5737 },
5738 EditControlPointSpline {
5739 points: ast::Expr,
5740 construction: Option<bool>,
5741 },
5742 EditConstraintValue {
5743 value: ast::BinaryPart,
5744 },
5745 EditDistanceConstraintLabelPosition {
5746 label_position: ast::Expr,
5747 },
5748 EditCallUnlabeled {
5749 arg: ast::Expr,
5750 },
5751 EditVarInitialValue {
5752 value: Number,
5753 },
5754 DeleteNode,
5755}
5756
5757impl AstMutateCommand {
5758 fn needs_defined_names_stack(&self) -> bool {
5759 matches!(
5760 self,
5761 AstMutateCommand::AddSketchBlockVarDecl { .. } | AstMutateCommand::AddVariableDeclaration { .. }
5762 )
5763 }
5764}
5765
5766#[derive(Debug)]
5767enum AstMutateCommandReturn {
5768 None,
5769 Name(String),
5770}
5771
5772#[derive(Debug, Clone)]
5773struct AstNodeRef {
5774 range: SourceRange,
5775 node_path: Option<ast::NodePath>,
5776}
5777
5778impl<T> From<&ast::Node<T>> for AstNodeRef {
5779 fn from(value: &ast::Node<T>) -> Self {
5780 AstNodeRef {
5781 range: value.into(),
5782 node_path: value.node_path.clone(),
5783 }
5784 }
5785}
5786
5787impl From<&ast::BodyItem> for AstNodeRef {
5788 fn from(value: &ast::BodyItem) -> Self {
5789 match value {
5790 ast::BodyItem::ImportStatement(node) => AstNodeRef {
5791 range: node.into(),
5792 node_path: node.node_path.clone(),
5793 },
5794 ast::BodyItem::ExpressionStatement(node) => AstNodeRef {
5795 range: node.into(),
5796 node_path: node.node_path.clone(),
5797 },
5798 ast::BodyItem::VariableDeclaration(node) => AstNodeRef {
5799 range: node.into(),
5800 node_path: node.node_path.clone(),
5801 },
5802 ast::BodyItem::TypeDeclaration(node) => AstNodeRef {
5803 range: node.into(),
5804 node_path: node.node_path.clone(),
5805 },
5806 ast::BodyItem::ReturnStatement(node) => AstNodeRef {
5807 range: node.into(),
5808 node_path: node.node_path.clone(),
5809 },
5810 }
5811 }
5812}
5813
5814impl From<&ast::Expr> for AstNodeRef {
5815 fn from(value: &ast::Expr) -> Self {
5816 AstNodeRef {
5817 range: SourceRange::from(value),
5818 node_path: value.node_path().cloned(),
5819 }
5820 }
5821}
5822
5823impl From<&AstMutateContext> for AstNodeRef {
5824 fn from(value: &AstMutateContext) -> Self {
5825 AstNodeRef {
5826 range: value.source_range,
5827 node_path: value.node_path.clone(),
5828 }
5829 }
5830}
5831
5832impl TryFrom<&NodeMut<'_>> for AstNodeRef {
5833 type Error = crate::walk::AstNodeError;
5834
5835 fn try_from(value: &NodeMut<'_>) -> Result<Self, Self::Error> {
5836 Ok(AstNodeRef {
5837 range: SourceRange::try_from(value)?,
5838 node_path: value.try_into()?,
5839 })
5840 }
5841}
5842
5843impl From<AstNodeRef> for SourceRange {
5844 fn from(value: AstNodeRef) -> Self {
5845 value.range
5846 }
5847}
5848
5849impl Visitor for AstMutateContext {
5850 type Break = Result<(AstNodeRef, AstMutateCommandReturn), KclError>;
5851 type Continue = ();
5852
5853 fn visit(&mut self, node: NodeMut<'_>) -> TraversalReturn<Self::Break, Self::Continue> {
5854 filter_and_process(self, node)
5855 }
5856
5857 fn finish(&mut self, node: NodeMut<'_>) {
5858 match &node {
5859 NodeMut::Program(_) | NodeMut::SketchBlock(_) => {
5860 self.defined_names_stack.pop();
5861 }
5862 _ => {}
5863 }
5864 }
5865}
5866
5867fn filter_and_process(
5868 ctx: &mut AstMutateContext,
5869 node: NodeMut,
5870) -> TraversalReturn<Result<(AstNodeRef, AstMutateCommandReturn), KclError>> {
5871 let Ok(node_range) = SourceRange::try_from(&node) else {
5872 return TraversalReturn::new_continue(());
5874 };
5875 if let NodeMut::VariableDeclaration(var_decl) = &node {
5880 let expr_range = SourceRange::from(&var_decl.declaration.init);
5881 let expr_node_path = var_decl.declaration.init.node_path();
5882 if source_ref_matches(ctx, expr_range, expr_node_path) {
5883 if let AstMutateCommand::AddVariableDeclaration { .. } = &ctx.command {
5884 return TraversalReturn::new_break(Ok((
5887 AstNodeRef::from(&**var_decl),
5888 AstMutateCommandReturn::Name(var_decl.name().to_owned()),
5889 )));
5890 }
5891 if let AstMutateCommand::DeleteNode = &ctx.command {
5892 return TraversalReturn {
5895 mutate_body_item: MutateBodyItem::Delete,
5896 control_flow: ControlFlow::Break(Ok((AstNodeRef::from(&*ctx), AstMutateCommandReturn::None))),
5897 };
5898 }
5899 }
5900 }
5901 if let NodeMut::ExpressionStatement(expr_stmt) = &node {
5904 let expr_range = SourceRange::from(&expr_stmt.expression);
5905 let expr_node_path = expr_stmt.expression.node_path();
5906 if source_ref_matches(ctx, expr_range, expr_node_path) {
5907 if let AstMutateCommand::AddVariableDeclaration { .. } = &ctx.command {
5908 let Ok(node_ref) = AstNodeRef::try_from(&node) else {
5911 return TraversalReturn::new_continue(());
5912 };
5913 return process(ctx, node).map_break(|result| result.map(|cmd_return| (node_ref, cmd_return)));
5914 }
5915 if let AstMutateCommand::DeleteNode = &ctx.command {
5916 return TraversalReturn {
5919 mutate_body_item: MutateBodyItem::Delete,
5920 control_flow: ControlFlow::Break(Ok((AstNodeRef::from(&*ctx), AstMutateCommandReturn::None))),
5921 };
5922 }
5923 }
5924 }
5925
5926 if ctx.command.needs_defined_names_stack() {
5927 if let NodeMut::Program(program) = &node {
5928 ctx.defined_names_stack.push(find_defined_names(*program));
5929 } else if let NodeMut::SketchBlock(block) = &node {
5930 ctx.defined_names_stack.push(find_defined_names(&block.body));
5931 }
5932 }
5933
5934 let node_path = <Option<ast::NodePath>>::try_from(&node).ok().flatten();
5936 if !source_ref_matches(ctx, node_range, node_path.as_ref()) {
5937 return TraversalReturn::new_continue(());
5938 }
5939 let Ok(node_ref) = AstNodeRef::try_from(&node) else {
5940 return TraversalReturn::new_continue(());
5941 };
5942 process(ctx, node).map_break(|result| result.map(|cmd_return| (node_ref, cmd_return)))
5943}
5944
5945fn source_ref_matches(ctx: &AstMutateContext, node_range: SourceRange, node_path: Option<&ast::NodePath>) -> bool {
5946 match &ctx.node_path {
5947 Some(target) => Some(target) == node_path,
5948 None => node_range == ctx.source_range,
5949 }
5950}
5951
5952fn process(ctx: &AstMutateContext, node: NodeMut) -> TraversalReturn<Result<AstMutateCommandReturn, KclError>> {
5953 match &ctx.command {
5954 AstMutateCommand::AddSketchBlockExprStmt { expr } => {
5955 if let NodeMut::SketchBlock(sketch_block) = node {
5956 sketch_block
5957 .body
5958 .items
5959 .push(ast::BodyItem::ExpressionStatement(ast::Node {
5960 inner: ast::ExpressionStatement {
5961 expression: expr.clone(),
5962 digest: None,
5963 },
5964 start: Default::default(),
5965 end: Default::default(),
5966 module_id: Default::default(),
5967 node_path: None,
5968 outer_attrs: Default::default(),
5969 pre_comments: Default::default(),
5970 comment_start: Default::default(),
5971 }));
5972 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5973 }
5974 }
5975 AstMutateCommand::AddSketchBlockVarDecl { prefix, expr } => {
5976 if let NodeMut::SketchBlock(sketch_block) = node {
5977 let empty_defined_names = HashSet::new();
5978 let defined_names = ctx.defined_names_stack.last().unwrap_or(&empty_defined_names);
5979 let Ok(name) = next_free_name(prefix, defined_names) else {
5980 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5981 };
5982 sketch_block
5983 .body
5984 .items
5985 .push(ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(
5986 ast::VariableDeclaration::new(
5987 ast::VariableDeclarator::new(&name, expr.clone()),
5988 ast::ItemVisibility::Default,
5989 ast::VariableKind::Const,
5990 ),
5991 ))));
5992 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::Name(name)));
5993 }
5994 }
5995 AstMutateCommand::AddVariableDeclaration { prefix } => {
5996 if let NodeMut::VariableDeclaration(inner) = node {
5997 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::Name(inner.name().to_owned())));
5998 }
5999 if let NodeMut::ExpressionStatement(expr_stmt) = node {
6000 let empty_defined_names = HashSet::new();
6001 let defined_names = ctx.defined_names_stack.last().unwrap_or(&empty_defined_names);
6002 let Ok(name) = next_free_name(prefix, defined_names) else {
6003 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6005 };
6006 let mutate_node =
6007 ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(ast::VariableDeclaration::new(
6008 ast::VariableDeclarator::new(&name, expr_stmt.expression.clone()),
6009 ast::ItemVisibility::Default,
6010 ast::VariableKind::Const,
6011 ))));
6012 return TraversalReturn {
6013 mutate_body_item: MutateBodyItem::Mutate(Box::new(mutate_node)),
6014 control_flow: ControlFlow::Break(Ok(AstMutateCommandReturn::Name(name))),
6015 };
6016 }
6017 }
6018 AstMutateCommand::EditPoint { at } => {
6019 if let NodeMut::CallExpressionKw(call) = node {
6020 if call.callee.name.name != POINT_FN {
6021 return TraversalReturn::new_continue(());
6022 }
6023 for labeled_arg in &mut call.arguments {
6025 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(POINT_AT_PARAM) {
6026 labeled_arg.arg = at.clone();
6027 }
6028 }
6029 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6030 }
6031 }
6032 AstMutateCommand::EditLine {
6033 start,
6034 end,
6035 construction,
6036 } => {
6037 if let NodeMut::CallExpressionKw(call) = node {
6038 if call.callee.name.name != LINE_FN {
6039 return TraversalReturn::new_continue(());
6040 }
6041 for labeled_arg in &mut call.arguments {
6043 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(LINE_START_PARAM) {
6044 labeled_arg.arg = start.clone();
6045 }
6046 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(LINE_END_PARAM) {
6047 labeled_arg.arg = end.clone();
6048 }
6049 }
6050 if let Some(construction_value) = construction {
6052 let construction_exists = call
6053 .arguments
6054 .iter()
6055 .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
6056 if *construction_value {
6057 if construction_exists {
6059 for labeled_arg in &mut call.arguments {
6061 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
6062 labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
6063 value: ast::LiteralValue::Bool(true),
6064 raw: "true".to_string(),
6065 digest: None,
6066 })));
6067 }
6068 }
6069 } else {
6070 call.arguments.push(ast::LabeledArg {
6072 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
6073 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
6074 value: ast::LiteralValue::Bool(true),
6075 raw: "true".to_string(),
6076 digest: None,
6077 }))),
6078 });
6079 }
6080 } else {
6081 call.arguments
6083 .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
6084 }
6085 }
6086 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6087 }
6088 }
6089 AstMutateCommand::EditArc {
6090 start,
6091 end,
6092 center,
6093 construction,
6094 } => {
6095 if let NodeMut::CallExpressionKw(call) = node {
6096 if call.callee.name.name != ARC_FN {
6097 return TraversalReturn::new_continue(());
6098 }
6099 for labeled_arg in &mut call.arguments {
6101 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_START_PARAM) {
6102 labeled_arg.arg = start.clone();
6103 }
6104 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_END_PARAM) {
6105 labeled_arg.arg = end.clone();
6106 }
6107 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_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 {
6120 for labeled_arg in &mut call.arguments {
6122 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
6123 labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
6124 value: ast::LiteralValue::Bool(true),
6125 raw: "true".to_string(),
6126 digest: None,
6127 })));
6128 }
6129 }
6130 } else {
6131 call.arguments.push(ast::LabeledArg {
6133 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
6134 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
6135 value: ast::LiteralValue::Bool(true),
6136 raw: "true".to_string(),
6137 digest: None,
6138 }))),
6139 });
6140 }
6141 } else {
6142 call.arguments
6144 .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
6145 }
6146 }
6147 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6148 }
6149 }
6150 AstMutateCommand::EditCircle {
6151 start,
6152 center,
6153 construction,
6154 } => {
6155 if let NodeMut::CallExpressionKw(call) = node {
6156 if call.callee.name.name != CIRCLE_FN {
6157 return TraversalReturn::new_continue(());
6158 }
6159 for labeled_arg in &mut call.arguments {
6161 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CIRCLE_START_PARAM) {
6162 labeled_arg.arg = start.clone();
6163 }
6164 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CIRCLE_CENTER_PARAM) {
6165 labeled_arg.arg = center.clone();
6166 }
6167 }
6168 if let Some(construction_value) = construction {
6170 let construction_exists = call
6171 .arguments
6172 .iter()
6173 .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
6174 if *construction_value {
6175 if construction_exists {
6176 for labeled_arg in &mut call.arguments {
6178 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
6179 labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
6180 value: ast::LiteralValue::Bool(true),
6181 raw: "true".to_string(),
6182 digest: None,
6183 })));
6184 }
6185 }
6186 } else {
6187 call.arguments.push(ast::LabeledArg {
6189 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
6190 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
6191 value: ast::LiteralValue::Bool(true),
6192 raw: "true".to_string(),
6193 digest: None,
6194 }))),
6195 });
6196 }
6197 } else {
6198 call.arguments
6200 .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
6201 }
6202 }
6203 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6204 }
6205 }
6206 AstMutateCommand::EditControlPointSpline { points, construction } => {
6207 if let NodeMut::CallExpressionKw(call) = node {
6208 if call.callee.name.name != CONTROL_POINT_SPLINE_FN {
6209 return TraversalReturn::new_continue(());
6210 }
6211 for labeled_arg in &mut call.arguments {
6212 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONTROL_POINT_SPLINE_POINTS_PARAM)
6213 {
6214 labeled_arg.arg = points.clone();
6215 }
6216 }
6217 if let Some(construction_value) = construction {
6219 let construction_exists = call
6220 .arguments
6221 .iter()
6222 .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
6223 if *construction_value {
6224 if construction_exists {
6225 for labeled_arg in &mut call.arguments {
6226 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
6227 labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
6228 value: ast::LiteralValue::Bool(true),
6229 raw: "true".to_string(),
6230 digest: None,
6231 })));
6232 }
6233 }
6234 } else {
6235 call.arguments.push(ast::LabeledArg {
6236 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
6237 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
6238 value: ast::LiteralValue::Bool(true),
6239 raw: "true".to_string(),
6240 digest: None,
6241 }))),
6242 });
6243 }
6244 } else {
6245 call.arguments
6246 .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
6247 }
6248 }
6249 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6250 }
6251 }
6252 AstMutateCommand::EditConstraintValue { value } => {
6253 if let NodeMut::BinaryExpression(binary_expr) = node {
6254 let left_is_constraint = matches!(
6255 &binary_expr.left,
6256 ast::BinaryPart::CallExpressionKw(call)
6257 if matches!(
6258 call.callee.name.name.as_str(),
6259 DISTANCE_FN | HORIZONTAL_DISTANCE_FN | VERTICAL_DISTANCE_FN | RADIUS_FN | DIAMETER_FN | ANGLE_FN
6260 )
6261 );
6262 if left_is_constraint {
6263 binary_expr.right = value.clone();
6264 } else {
6265 binary_expr.left = value.clone();
6266 }
6267
6268 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6269 }
6270 }
6271 AstMutateCommand::EditDistanceConstraintLabelPosition { label_position } => {
6272 if let NodeMut::BinaryExpression(binary_expr) = node {
6273 let ast::BinaryPart::CallExpressionKw(call) = &mut binary_expr.left else {
6274 return TraversalReturn::new_continue(());
6275 };
6276 if !matches!(
6277 call.callee.name.name.as_str(),
6278 DISTANCE_FN | HORIZONTAL_DISTANCE_FN | VERTICAL_DISTANCE_FN | RADIUS_FN | DIAMETER_FN
6279 ) {
6280 return TraversalReturn::new_continue(());
6281 }
6282
6283 if let Some(label_arg) = call
6284 .arguments
6285 .iter_mut()
6286 .find(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(LABEL_POSITION_PARAM))
6287 {
6288 label_arg.arg = label_position.clone();
6289 } else {
6290 call.arguments.push(ast::LabeledArg {
6291 label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
6292 arg: label_position.clone(),
6293 });
6294 }
6295
6296 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6297 }
6298 }
6299 AstMutateCommand::EditCallUnlabeled { arg } => {
6300 if let NodeMut::CallExpressionKw(call) = node {
6301 call.unlabeled = Some(arg.clone());
6302 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6303 }
6304 }
6305 AstMutateCommand::EditVarInitialValue { value } => {
6306 if let NodeMut::SketchVar(sketch_var) = node {
6310 let Ok(literal) = to_source_number(*value) else {
6311 return TraversalReturn::new_break(Err(KclError::refactor(format!(
6312 "Could not convert number to AST literal: {:?}",
6313 *value
6314 ))));
6315 };
6316 sketch_var.initial = Some(Box::new(ast::Node::no_src(literal)));
6317 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6318 }
6319 }
6320 AstMutateCommand::DeleteNode => {
6321 return TraversalReturn {
6322 mutate_body_item: MutateBodyItem::Delete,
6323 control_flow: ControlFlow::Break(Ok(AstMutateCommandReturn::None)),
6324 };
6325 }
6326 }
6327 TraversalReturn::new_continue(())
6328}
6329
6330struct FindSketchBlockSourceRange {
6331 target_before_mutation: SourceRange,
6333 found: Cell<Option<AstNodeRef>>,
6337}
6338
6339impl<'a> crate::walk::Visitor<'a> for &FindSketchBlockSourceRange {
6340 type Error = crate::front::Error;
6341
6342 fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
6343 let Ok(node_range) = SourceRange::try_from(&node) else {
6344 return Ok(true);
6345 };
6346
6347 if let crate::walk::Node::SketchBlock(sketch_block) = node {
6348 if node_range.module_id() == self.target_before_mutation.module_id()
6349 && node_range.start() == self.target_before_mutation.start()
6350 && node_range.end() >= self.target_before_mutation.end()
6352 {
6353 self.found.set(sketch_block.body.items.last().map(|item| match item {
6354 ast::BodyItem::VariableDeclaration(node) => AstNodeRef::from(&node.declaration.init),
6358 _ => AstNodeRef::from(item),
6359 }));
6360 return Ok(false);
6361 } else {
6362 return Ok(true);
6365 }
6366 }
6367
6368 for child in node.children().iter() {
6369 if !child.visit(*self)? {
6370 return Ok(false);
6371 }
6372 }
6373
6374 Ok(true)
6375 }
6376}
6377
6378struct FindSketchBlockByNodePath {
6379 target_node_path: ast::NodePath,
6381 found: Cell<Option<AstNodeRef>>,
6385}
6386
6387impl<'a> crate::walk::Visitor<'a> for &FindSketchBlockByNodePath {
6388 type Error = crate::front::Error;
6389
6390 fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
6391 let Ok(node_path) = <Option<ast::NodePath>>::try_from(&node) else {
6392 return Ok(true);
6393 };
6394
6395 if let crate::walk::Node::SketchBlock(sketch_block) = node {
6396 if let Some(node_path) = node_path
6397 && node_path == self.target_node_path
6398 {
6399 self.found.set(sketch_block.body.items.last().map(|item| match item {
6400 ast::BodyItem::VariableDeclaration(node) => AstNodeRef::from(&node.declaration.init),
6404 _ => AstNodeRef::from(item),
6405 }));
6406
6407 return Ok(false);
6408 } else {
6409 return Ok(true);
6412 }
6413 }
6414
6415 for child in node.children().iter() {
6416 if !child.visit(*self)? {
6417 return Ok(false);
6418 }
6419 }
6420
6421 Ok(true)
6422 }
6423}
6424
6425fn find_sketch_block_added_item(
6433 ast: &ast::Node<ast::Program>,
6434 sketch_block_before_mutation: &AstNodeRef,
6435) -> Result<AstNodeRef, KclError> {
6436 if let Some(node_path) = &sketch_block_before_mutation.node_path {
6437 let find = FindSketchBlockByNodePath {
6438 target_node_path: node_path.clone(),
6439 found: Cell::new(None),
6440 };
6441 let node = crate::walk::Node::from(ast);
6442 node.visit(&find).map_err(|err| KclError::refactor(err.msg))?;
6443 find.found.into_inner().ok_or_else(|| {
6444 KclError::refactor(format!(
6445 "Node ID after mutation not found for Node ID before mutation: {node_path:?}"
6446 ))
6447 })
6448 } else {
6449 let find = FindSketchBlockSourceRange {
6451 target_before_mutation: sketch_block_before_mutation.range,
6452 found: Cell::new(None),
6453 };
6454 let node = crate::walk::Node::from(ast);
6455 node.visit(&find).map_err(|err| KclError::refactor(err.msg))?;
6456 find.found.into_inner().ok_or_else(|| KclError::refactor(
6457 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?"),
6458 ))
6459 }
6460}
6461
6462fn source_from_ast(ast: &ast::Node<ast::Program>) -> String {
6463 ast.recast_top(&Default::default(), 0)
6465}
6466
6467struct FindNumericLiteral {
6468 target: SourceRange,
6469 found: Cell<Option<ast::NumericLiteral>>,
6470}
6471
6472impl<'a> crate::walk::Visitor<'a> for &FindNumericLiteral {
6473 type Error = crate::front::Error;
6474
6475 fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
6476 let Ok(node_range) = SourceRange::try_from(&node) else {
6477 return Ok(true);
6478 };
6479
6480 if node_range == self.target
6481 && let crate::walk::Node::NumericLiteral(literal) = node
6482 {
6483 self.found.set(Some(literal.inner.clone()));
6484 return Ok(false);
6485 }
6486
6487 for child in node.children().iter() {
6488 if !child.visit(*self)? {
6489 return Ok(false);
6490 }
6491 }
6492
6493 Ok(true)
6494 }
6495}
6496
6497fn numeric_literal_at_source_range(ast: &ast::Node<ast::Program>, target: SourceRange) -> Option<ast::NumericLiteral> {
6498 let find = FindNumericLiteral {
6499 target,
6500 found: Cell::new(None),
6501 };
6502 let node = crate::walk::Node::from(ast);
6503 node.visit(&find).ok()?;
6504 find.found.into_inner()
6505}
6506
6507struct FindSketchVarInitialByNodePath<'a> {
6508 target: &'a ast::NodePath,
6509 sketch_var_found: Cell<bool>,
6510 initial_literal: Cell<Option<ast::NumericLiteral>>,
6511}
6512
6513impl<'a, 'b> crate::walk::Visitor<'b> for &FindSketchVarInitialByNodePath<'a> {
6514 type Error = crate::front::Error;
6515
6516 fn visit_node(&self, node: crate::walk::Node<'b>) -> anyhow::Result<bool, Self::Error> {
6517 if let crate::walk::Node::SketchVar(sketch_var) = node
6518 && sketch_var.node_path.as_ref() == Some(self.target)
6519 {
6520 self.sketch_var_found.set(true);
6521 if let Some(initial) = &sketch_var.initial {
6522 self.initial_literal.set(Some(initial.inner.clone()));
6523 }
6524 return Ok(false);
6525 }
6526
6527 for child in node.children().iter() {
6528 if !child.visit(*self)? {
6529 return Ok(false);
6530 }
6531 }
6532
6533 Ok(true)
6534 }
6535}
6536
6537fn numeric_literal_at_node_path(
6547 ast: &ast::Node<ast::Program>,
6548 node_path: Option<&ast::NodePath>,
6549 source_range: SourceRange,
6550) -> Option<Option<ast::NumericLiteral>> {
6551 let Some(node_path) = node_path else {
6552 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";
6553 #[cfg(target_arch = "wasm32")]
6554 web_sys::console::warn_1(&message.into());
6555 #[cfg(not(target_arch = "wasm32"))]
6556 eprintln!("WARNING: {message}");
6557 return numeric_literal_at_source_range(ast, source_range).map(Some);
6558 };
6559 let find = FindSketchVarInitialByNodePath {
6560 target: node_path,
6561 sketch_var_found: Cell::new(false),
6562 initial_literal: Cell::new(None),
6563 };
6564 let node = crate::walk::Node::from(ast);
6565 node.visit(&find).ok()?;
6566 if !find.sketch_var_found.get() {
6567 return None;
6568 }
6569 Some(find.initial_literal.into_inner())
6570}
6571
6572fn suffix_length_unit(suffix: NumericSuffix) -> Option<UnitLength> {
6573 match suffix {
6574 NumericSuffix::Mm => Some(UnitLength::Millimeters),
6575 NumericSuffix::Cm => Some(UnitLength::Centimeters),
6576 NumericSuffix::M => Some(UnitLength::Meters),
6577 NumericSuffix::Inch => Some(UnitLength::Inches),
6578 NumericSuffix::Ft => Some(UnitLength::Feet),
6579 NumericSuffix::Yd => Some(UnitLength::Yards),
6580 _ => None,
6581 }
6582}
6583
6584fn number_value_in_default_length_units(number: Number, default_length_unit: UnitLength) -> f64 {
6585 match suffix_length_unit(number.units) {
6586 Some(unit) => adjust_length(unit, number.value, default_length_unit).0,
6587 None => number.value,
6588 }
6589}
6590
6591fn literal_value_in_default_length_units(literal: &ast::NumericLiteral, default_length_unit: UnitLength) -> f64 {
6592 match suffix_length_unit(literal.suffix) {
6593 Some(unit) => adjust_length(unit, literal.value, default_length_unit).0,
6594 None => literal.value,
6595 }
6596}
6597
6598fn var_solution_needs_commit(
6599 current_literal: &ast::NumericLiteral,
6600 solved_value: Number,
6601 default_length_unit: UnitLength,
6602) -> bool {
6603 let current = literal_value_in_default_length_units(current_literal, default_length_unit);
6604 let solved = number_value_in_default_length_units(solved_value, default_length_unit);
6605
6606 (current - solved).abs() > 1e-9
6607}
6608
6609fn preserve_var_solution_literal_style(
6610 current_literal: &ast::NumericLiteral,
6611 solved_value: Number,
6612 default_length_unit: UnitLength,
6613) -> Number {
6614 if current_literal.suffix == NumericSuffix::None {
6615 return Number {
6616 value: number_value_in_default_length_units(solved_value, default_length_unit),
6617 units: NumericSuffix::None,
6618 };
6619 }
6620
6621 let Some(current_unit) = suffix_length_unit(current_literal.suffix) else {
6622 return solved_value;
6623 };
6624
6625 let solved_default_value = number_value_in_default_length_units(solved_value, default_length_unit);
6626 Number {
6627 value: adjust_length(default_length_unit, solved_default_value, current_unit).0,
6628 units: current_literal.suffix,
6629 }
6630}
6631
6632pub(crate) fn to_ast_point2d(point: &Point2d<Expr>) -> anyhow::Result<ast::Expr> {
6633 Ok(ast::Expr::ArrayExpression(Box::new(ast::Node {
6634 inner: ast::ArrayExpression {
6635 elements: vec![to_source_expr(&point.x)?, to_source_expr(&point.y)?],
6636 non_code_meta: Default::default(),
6637 digest: None,
6638 },
6639 start: Default::default(),
6640 end: Default::default(),
6641 module_id: Default::default(),
6642 node_path: None,
6643 outer_attrs: Default::default(),
6644 pre_comments: Default::default(),
6645 comment_start: Default::default(),
6646 })))
6647}
6648
6649pub(crate) fn to_ast_point2d_array(points: &[Point2d<Expr>]) -> anyhow::Result<ast::Expr> {
6650 Ok(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
6651 ast::ArrayExpression {
6652 elements: points.iter().map(to_ast_point2d).collect::<anyhow::Result<Vec<_>>>()?,
6653 digest: None,
6654 non_code_meta: Default::default(),
6655 },
6656 ))))
6657}
6658
6659fn to_ast_point2d_number(point: &Point2d<Number>) -> anyhow::Result<ast::Expr> {
6660 Ok(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
6661 ast::ArrayExpression {
6662 elements: vec![
6663 ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(to_source_number(
6664 point.x,
6665 )?)))),
6666 ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(to_source_number(
6667 point.y,
6668 )?)))),
6669 ],
6670 non_code_meta: Default::default(),
6671 digest: None,
6672 },
6673 ))))
6674}
6675
6676fn to_source_expr(expr: &Expr) -> anyhow::Result<ast::Expr> {
6677 match expr {
6678 Expr::Number(number) => Ok(ast::Expr::Literal(Box::new(ast::Node {
6679 inner: ast::Literal::from(to_source_number(*number)?),
6680 start: Default::default(),
6681 end: Default::default(),
6682 module_id: Default::default(),
6683 node_path: None,
6684 outer_attrs: Default::default(),
6685 pre_comments: Default::default(),
6686 comment_start: Default::default(),
6687 }))),
6688 Expr::Var(number) => Ok(ast::Expr::SketchVar(Box::new(ast::Node {
6689 inner: ast::SketchVar {
6690 initial: Some(Box::new(ast::Node {
6691 inner: to_source_number(*number)?,
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 digest: None,
6701 },
6702 start: Default::default(),
6703 end: Default::default(),
6704 module_id: Default::default(),
6705 node_path: None,
6706 outer_attrs: Default::default(),
6707 pre_comments: Default::default(),
6708 comment_start: Default::default(),
6709 }))),
6710 Expr::Variable(variable) => Ok(ast_name_expr(variable.clone())),
6711 }
6712}
6713
6714fn to_source_number(number: Number) -> anyhow::Result<ast::NumericLiteral> {
6715 Ok(ast::NumericLiteral {
6716 value: number.value,
6717 suffix: number.units,
6718 raw: format_number_literal(number.value, number.units, None)?,
6719 digest: None,
6720 })
6721}
6722
6723pub(crate) fn ast_name_expr(name: String) -> ast::Expr {
6724 ast::Expr::Name(Box::new(ast_name(name)))
6725}
6726
6727fn ast_name(name: String) -> ast::Node<ast::Name> {
6728 ast::Node {
6729 inner: ast::Name {
6730 name: ast::Node {
6731 inner: ast::Identifier { name, digest: None },
6732 start: Default::default(),
6733 end: Default::default(),
6734 module_id: Default::default(),
6735 node_path: None,
6736 outer_attrs: Default::default(),
6737 pre_comments: Default::default(),
6738 comment_start: Default::default(),
6739 },
6740 path: Vec::new(),
6741 abs_path: false,
6742 digest: None,
6743 },
6744 start: Default::default(),
6745 end: Default::default(),
6746 module_id: Default::default(),
6747 node_path: None,
6748 outer_attrs: Default::default(),
6749 pre_comments: Default::default(),
6750 comment_start: Default::default(),
6751 }
6752}
6753
6754pub(crate) fn ast_sketch2_name(name: &str) -> ast::Name {
6755 ast::Name {
6756 name: ast::Node {
6757 inner: ast::Identifier {
6758 name: name.to_owned(),
6759 digest: None,
6760 },
6761 start: Default::default(),
6762 end: Default::default(),
6763 module_id: Default::default(),
6764 node_path: None,
6765 outer_attrs: Default::default(),
6766 pre_comments: Default::default(),
6767 comment_start: Default::default(),
6768 },
6769 path: Default::default(),
6770 abs_path: false,
6771 digest: None,
6772 }
6773}
6774
6775pub(crate) fn create_coincident_ast(exprs: impl IntoIterator<Item = ast::Expr>) -> ast::Expr {
6779 let elements = exprs.into_iter().collect::<Vec<_>>();
6780 debug_assert!(elements.len() >= 2, "Coincident AST should have at least 2 inputs");
6781
6782 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
6784 elements,
6785 digest: None,
6786 non_code_meta: Default::default(),
6787 })));
6788
6789 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6791 callee: ast::Node::no_src(ast_sketch2_name(COINCIDENT_FN)),
6792 unlabeled: Some(array_expr),
6793 arguments: Default::default(),
6794 digest: None,
6795 non_code_meta: Default::default(),
6796 })))
6797}
6798
6799pub(crate) fn create_line_ast(start_ast: ast::Expr, end_ast: ast::Expr) -> ast::Expr {
6801 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6802 callee: ast::Node::no_src(ast_sketch2_name(LINE_FN)),
6803 unlabeled: None,
6804 arguments: vec![
6805 ast::LabeledArg {
6806 label: Some(ast::Identifier::new(LINE_START_PARAM)),
6807 arg: start_ast,
6808 },
6809 ast::LabeledArg {
6810 label: Some(ast::Identifier::new(LINE_END_PARAM)),
6811 arg: end_ast,
6812 },
6813 ],
6814 digest: None,
6815 non_code_meta: Default::default(),
6816 })))
6817}
6818
6819pub(crate) fn create_arc_ast(start_ast: ast::Expr, end_ast: ast::Expr, center_ast: ast::Expr) -> ast::Expr {
6821 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6822 callee: ast::Node::no_src(ast_sketch2_name(ARC_FN)),
6823 unlabeled: None,
6824 arguments: vec![
6825 ast::LabeledArg {
6826 label: Some(ast::Identifier::new(ARC_START_PARAM)),
6827 arg: start_ast,
6828 },
6829 ast::LabeledArg {
6830 label: Some(ast::Identifier::new(ARC_END_PARAM)),
6831 arg: end_ast,
6832 },
6833 ast::LabeledArg {
6834 label: Some(ast::Identifier::new(ARC_CENTER_PARAM)),
6835 arg: center_ast,
6836 },
6837 ],
6838 digest: None,
6839 non_code_meta: Default::default(),
6840 })))
6841}
6842
6843pub(crate) fn create_circle_ast(start_ast: ast::Expr, center_ast: ast::Expr) -> ast::Expr {
6845 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6846 callee: ast::Node::no_src(ast_sketch2_name(CIRCLE_FN)),
6847 unlabeled: None,
6848 arguments: vec![
6849 ast::LabeledArg {
6850 label: Some(ast::Identifier::new(CIRCLE_START_PARAM)),
6851 arg: start_ast,
6852 },
6853 ast::LabeledArg {
6854 label: Some(ast::Identifier::new(CIRCLE_CENTER_PARAM)),
6855 arg: center_ast,
6856 },
6857 ],
6858 digest: None,
6859 non_code_meta: Default::default(),
6860 })))
6861}
6862
6863pub(crate) fn create_horizontal_ast(line_expr: ast::Expr) -> ast::Expr {
6865 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6866 callee: ast::Node::no_src(ast_sketch2_name(HORIZONTAL_FN)),
6867 unlabeled: Some(line_expr),
6868 arguments: Default::default(),
6869 digest: None,
6870 non_code_meta: Default::default(),
6871 })))
6872}
6873
6874pub(crate) fn create_vertical_ast(line_expr: ast::Expr) -> ast::Expr {
6876 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6877 callee: ast::Node::no_src(ast_sketch2_name(VERTICAL_FN)),
6878 unlabeled: Some(line_expr),
6879 arguments: Default::default(),
6880 digest: None,
6881 non_code_meta: Default::default(),
6882 })))
6883}
6884
6885pub(crate) fn create_member_expression(object_expr: ast::Expr, property: &str) -> ast::Expr {
6887 ast::Expr::MemberExpression(Box::new(ast::Node::no_src(ast::MemberExpression {
6888 object: object_expr,
6889 property: ast::Expr::Name(Box::new(ast::Node::no_src(ast::Name {
6890 name: ast::Node::no_src(ast::Identifier {
6891 name: property.to_string(),
6892 digest: None,
6893 }),
6894 path: Vec::new(),
6895 abs_path: false,
6896 digest: None,
6897 }))),
6898 computed: false,
6899 digest: None,
6900 })))
6901}
6902
6903pub(crate) fn create_index_expression(object_expr: ast::Expr, index: usize) -> ast::Expr {
6904 ast::Expr::MemberExpression(Box::new(ast::Node::no_src(ast::MemberExpression {
6905 object: object_expr,
6906 property: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(ast::NumericLiteral {
6907 value: index as f64,
6908 suffix: NumericSuffix::None,
6909 raw: index.to_string(),
6910 digest: None,
6911 })))),
6912 computed: true,
6913 digest: None,
6914 })))
6915}
6916
6917fn create_fixed_point_constraint_ast(point_expr: ast::Expr, position: Point2d<Number>) -> anyhow::Result<ast::Expr> {
6919 let x_literal = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(to_source_number(
6921 position.x,
6922 )?))));
6923 let y_literal = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(to_source_number(
6924 position.y,
6925 )?))));
6926 let point_array = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
6927 elements: vec![x_literal, y_literal],
6928 digest: None,
6929 non_code_meta: Default::default(),
6930 })));
6931
6932 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
6934 elements: vec![point_expr, point_array],
6935 digest: None,
6936 non_code_meta: Default::default(),
6937 })));
6938
6939 Ok(ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(
6941 ast::CallExpressionKw {
6942 callee: ast::Node::no_src(ast_sketch2_name(FIXED_FN)),
6943 unlabeled: Some(array_expr),
6944 arguments: Default::default(),
6945 digest: None,
6946 non_code_meta: Default::default(),
6947 },
6948 ))))
6949}
6950
6951pub(crate) fn create_equal_length_ast(line_exprs: Vec<ast::Expr>) -> ast::Expr {
6953 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
6954 elements: line_exprs,
6955 digest: None,
6956 non_code_meta: Default::default(),
6957 })));
6958
6959 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6961 callee: ast::Node::no_src(ast_sketch2_name(EQUAL_LENGTH_FN)),
6962 unlabeled: Some(array_expr),
6963 arguments: Default::default(),
6964 digest: None,
6965 non_code_meta: Default::default(),
6966 })))
6967}
6968
6969pub(crate) fn create_equal_radius_ast(segment_exprs: Vec<ast::Expr>) -> ast::Expr {
6971 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
6972 elements: segment_exprs,
6973 digest: None,
6974 non_code_meta: Default::default(),
6975 })));
6976
6977 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6978 callee: ast::Node::no_src(ast_sketch2_name(EQUAL_RADIUS_FN)),
6979 unlabeled: Some(array_expr),
6980 arguments: Default::default(),
6981 digest: None,
6982 non_code_meta: Default::default(),
6983 })))
6984}
6985
6986pub(crate) fn create_tangent_ast(seg1_expr: ast::Expr, seg2_expr: ast::Expr) -> ast::Expr {
6988 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
6989 elements: vec![seg1_expr, seg2_expr],
6990 digest: None,
6991 non_code_meta: Default::default(),
6992 })));
6993
6994 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6995 callee: ast::Node::no_src(ast_sketch2_name(TANGENT_FN)),
6996 unlabeled: Some(array_expr),
6997 arguments: Default::default(),
6998 digest: None,
6999 non_code_meta: Default::default(),
7000 })))
7001}
7002
7003pub(crate) fn create_symmetric_ast(input_exprs: Vec<ast::Expr>, axis_expr: ast::Expr) -> ast::Expr {
7005 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
7006 elements: input_exprs,
7007 digest: None,
7008 non_code_meta: Default::default(),
7009 })));
7010 let arguments = vec![ast::LabeledArg {
7011 label: Some(ast::Identifier::new(SYMMETRIC_AXIS_PARAM)),
7012 arg: axis_expr,
7013 }];
7014
7015 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
7016 callee: ast::Node::no_src(ast_sketch2_name(SYMMETRIC_FN)),
7017 unlabeled: Some(array_expr),
7018 arguments,
7019 digest: None,
7020 non_code_meta: Default::default(),
7021 })))
7022}
7023
7024pub(crate) fn create_midpoint_ast(segment_expr: ast::Expr, point_expr: ast::Expr) -> ast::Expr {
7026 let arguments = vec![ast::LabeledArg {
7027 label: Some(ast::Identifier::new(MIDPOINT_POINT_PARAM)),
7028 arg: point_expr,
7029 }];
7030
7031 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
7032 callee: ast::Node::no_src(ast_sketch2_name(MIDPOINT_FN)),
7033 unlabeled: Some(segment_expr),
7034 arguments,
7035 digest: None,
7036 non_code_meta: Default::default(),
7037 })))
7038}
7039
7040#[cfg(test)]
7041mod tests {
7042 use std::sync;
7043
7044 use super::*;
7045 use crate::engine::PlaneName;
7046 use crate::engine::engine_manager::EngineManager;
7047 use crate::execution::cache::SketchModeState;
7048 use crate::execution::cache::clear_mem_cache;
7049 use crate::execution::cache::read_old_memory;
7050 use crate::execution::cache::write_old_memory;
7051 use crate::front::Distance;
7052 use crate::front::Fixed;
7053 use crate::front::FixedPoint;
7054 use crate::front::Midpoint;
7055 use crate::front::Object;
7056 use crate::front::Plane;
7057 use crate::front::Sketch;
7058 use crate::front::Tangent;
7059 use crate::frontend::sketch::Vertical;
7060 use crate::pretty::NumericSuffix;
7061
7062 fn find_first_sketch_object(scene_graph: &SceneGraph) -> Option<&Object> {
7063 for object in &scene_graph.objects {
7064 if let ObjectKind::Sketch(_) = &object.kind {
7065 return Some(object);
7066 }
7067 }
7068 None
7069 }
7070
7071 fn find_first_face_object(scene_graph: &SceneGraph) -> Option<&Object> {
7072 for object in &scene_graph.objects {
7073 if let ObjectKind::Face(_) = &object.kind {
7074 return Some(object);
7075 }
7076 }
7077 None
7078 }
7079
7080 fn find_first_wall_object_id(scene_graph: &SceneGraph) -> Option<ObjectId> {
7081 for object in &scene_graph.objects {
7082 if matches!(&object.kind, ObjectKind::Wall(_)) {
7083 return Some(object.id);
7084 }
7085 }
7086 None
7087 }
7088
7089 fn find_cap_object_id_with_solid_output_index(
7090 scene_graph: &SceneGraph,
7091 cap_kind: crate::frontend::api::CapKind,
7092 solid_output_index: usize,
7093 ) -> Option<ObjectId> {
7094 for object in &scene_graph.objects {
7095 if matches!(&object.kind, ObjectKind::Cap(cap) if cap.kind == cap_kind && cap.solid_output_index == Some(solid_output_index))
7096 {
7097 return Some(object.id);
7098 }
7099 }
7100 None
7101 }
7102
7103 #[test]
7104 fn test_region_name_from_sweep_variable_supports_sweep_kinds() {
7105 let source = "\
7106region001 = region(point = [0.1, 0.1], sketch = s)
7107extrude001 = extrude(region001, length = 5)
7108revolve001 = revolve(region001, axis = Y)
7109sweep001 = sweep(region001, path = path001)
7110loft001 = loft(region001)
7111not_sweep001 = shell(extrude001, faces = [], thickness = 1)
7112";
7113
7114 let program = Program::parse(source).unwrap().0.unwrap();
7115
7116 assert_eq!(
7117 region_name_from_sweep_variable(&program.ast, "extrude001"),
7118 Some("region001".to_owned())
7119 );
7120 assert_eq!(
7121 region_name_from_sweep_variable(&program.ast, "revolve001"),
7122 Some("region001".to_owned())
7123 );
7124 assert_eq!(
7125 region_name_from_sweep_variable(&program.ast, "sweep001"),
7126 Some("region001".to_owned())
7127 );
7128 assert_eq!(
7129 region_name_from_sweep_variable(&program.ast, "loft001"),
7130 Some("region001".to_owned())
7131 );
7132 assert_eq!(region_name_from_sweep_variable(&program.ast, "not_sweep001"), None);
7133 }
7134
7135 #[track_caller]
7136 fn expect_sketch(object: &Object) -> &Sketch {
7137 if let ObjectKind::Sketch(sketch) = &object.kind {
7138 sketch
7139 } else {
7140 panic!("Object is not a sketch: {:?}", object);
7141 }
7142 }
7143
7144 fn point_position(scene_graph: &SceneGraph, point_id: ObjectId) -> Point2d<Number> {
7145 let point_object = scene_graph.objects.get(point_id.0).unwrap();
7146 let ObjectKind::Segment {
7147 segment: Segment::Point(point),
7148 } = &point_object.kind
7149 else {
7150 panic!("Object is not a point segment: {point_object:?}");
7151 };
7152 point.position.clone()
7153 }
7154
7155 fn assert_point_position_close(actual: Point2d<Number>, expected: Point2d<Number>) {
7156 assert!((actual.x.value - expected.x.value).abs() < 1e-6);
7157 assert!((actual.y.value - expected.y.value).abs() < 1e-6);
7158 }
7159
7160 fn point_expr_mm(x: f64, y: f64) -> Point2d<Expr> {
7163 Point2d {
7164 x: Expr::Var(Number {
7165 value: x,
7166 units: NumericSuffix::Mm,
7167 }),
7168 y: Expr::Var(Number {
7169 value: y,
7170 units: NumericSuffix::Mm,
7171 }),
7172 }
7173 }
7174
7175 fn point_number_mm(x: f64, y: f64) -> Point2d<Number> {
7178 Point2d {
7179 x: Number {
7180 value: x,
7181 units: NumericSuffix::Mm,
7182 },
7183 y: Number {
7184 value: y,
7185 units: NumericSuffix::Mm,
7186 },
7187 }
7188 }
7189
7190 fn make_line_ctor(start_x: f64, start_y: f64, end_x: f64, end_y: f64, units: NumericSuffix) -> LineCtor {
7191 LineCtor {
7192 start: Point2d {
7193 x: Expr::Number(Number { value: start_x, units }),
7194 y: Expr::Number(Number { value: start_y, units }),
7195 },
7196 end: Point2d {
7197 x: Expr::Number(Number { value: end_x, units }),
7198 y: Expr::Number(Number { value: end_y, units }),
7199 },
7200 construction: None,
7201 }
7202 }
7203
7204 async fn create_sketch_with_single_line(
7205 frontend: &mut FrontendState,
7206 ctx: &ExecutorContext,
7207 mock_ctx: &ExecutorContext,
7208 version: Version,
7209 ) -> (ObjectId, ObjectId, SourceDelta, SceneGraphDelta) {
7210 frontend.program = Program::empty();
7211
7212 let sketch_args = SketchCtor {
7213 on: Plane::Default(PlaneName::Xy),
7214 };
7215 let (_src_delta, _scene_delta, sketch_id) = frontend
7216 .new_sketch(ctx, ProjectId(0), FileId(0), version, sketch_args)
7217 .await
7218 .unwrap();
7219
7220 let segment = SegmentCtor::Line(make_line_ctor(0.0, 0.0, 10.0, 10.0, NumericSuffix::Mm));
7221 let (source_delta, scene_graph_delta) = frontend
7222 .add_segment(mock_ctx, version, sketch_id, segment, None)
7223 .await
7224 .unwrap();
7225 let line_id = *scene_graph_delta
7226 .new_objects
7227 .last()
7228 .expect("Expected line object id to be created");
7229
7230 (sketch_id, line_id, source_delta, scene_graph_delta)
7231 }
7232
7233 async fn seed_frontend_with_mock(frontend: &mut FrontendState, mock_ctx: &ExecutorContext, program: &Program) {
7234 frontend.program = program.clone();
7235 let outcome = mock_ctx.run_mock(program, &MockConfig::default()).await.unwrap();
7236 frontend.update_state_after_exec(outcome, true);
7237 }
7238
7239 #[tokio::test(flavor = "multi_thread")]
7240 async fn test_sketch_checkpoint_round_trip_restores_state() {
7241 let mut frontend = FrontendState::new();
7242 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7243 let mock_ctx = ExecutorContext::new_mock(None).await;
7244 let version = Version(0);
7245
7246 let (sketch_id, line_id, source_delta, scene_graph_delta) =
7247 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7248
7249 let expected_source = source_delta.text.clone();
7250 let expected_scene_graph = frontend.scene_graph.clone();
7251 let expected_exec_outcome = scene_graph_delta.exec_outcome.clone();
7252 let expected_point_freedom_cache = frontend.point_freedom_cache.clone();
7253
7254 let checkpoint_id = frontend
7255 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7256 .await
7257 .unwrap();
7258
7259 let edited_segments = vec![ExistingSegmentCtor {
7260 id: line_id,
7261 ctor: SegmentCtor::Line(make_line_ctor(1.0, 2.0, 13.0, 14.0, NumericSuffix::Mm)),
7262 }];
7263 let (edited_source, _edited_scene) = frontend
7264 .edit_segments(&mock_ctx, version, sketch_id, edited_segments)
7265 .await
7266 .unwrap();
7267 assert_ne!(edited_source.text, expected_source);
7268
7269 let restored = frontend.restore_sketch_checkpoint(checkpoint_id).await.unwrap();
7270
7271 assert_eq!(restored.source_delta.text, expected_source);
7272 assert_eq!(restored.scene_graph_delta.new_graph, expected_scene_graph);
7273 assert!(restored.scene_graph_delta.invalidates_ids);
7274 assert_eq!(restored.scene_graph_delta.exec_outcome, expected_exec_outcome);
7275 assert_eq!(frontend.scene_graph, expected_scene_graph);
7276 assert_eq!(frontend.point_freedom_cache, expected_point_freedom_cache);
7277
7278 ctx.close().await;
7279 }
7280
7281 #[tokio::test(flavor = "multi_thread")]
7282 async fn test_sketch_checkpoints_prune_oldest_entries() {
7283 let mut frontend = FrontendState::new();
7284 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7285 let mock_ctx = ExecutorContext::new_mock(None).await;
7286 let version = Version(0);
7287
7288 let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
7289 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7290
7291 let mut checkpoint_ids = Vec::new();
7292 for _ in 0..(MAX_SKETCH_CHECKPOINTS + 3) {
7293 checkpoint_ids.push(
7294 frontend
7295 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7296 .await
7297 .unwrap(),
7298 );
7299 }
7300
7301 assert_eq!(frontend.sketch_checkpoints.len(), MAX_SKETCH_CHECKPOINTS);
7302 assert!(checkpoint_ids.windows(2).all(|ids| ids[0] < ids[1]));
7303
7304 let oldest_retained = checkpoint_ids[3];
7305 assert_eq!(
7306 frontend.sketch_checkpoints.front().map(|checkpoint| checkpoint.id),
7307 Some(oldest_retained)
7308 );
7309
7310 let evicted_restore = frontend.restore_sketch_checkpoint(checkpoint_ids[0]).await;
7311 assert!(evicted_restore.is_err());
7312 assert!(evicted_restore.unwrap_err().msg.contains("Sketch checkpoint not found"));
7313
7314 frontend
7315 .restore_sketch_checkpoint(*checkpoint_ids.last().unwrap())
7316 .await
7317 .unwrap();
7318
7319 ctx.close().await;
7320 }
7321
7322 #[tokio::test(flavor = "multi_thread")]
7323 async fn test_restore_sketch_checkpoint_missing_id_returns_error() {
7324 let mut frontend = FrontendState::new();
7325 let missing_checkpoint = SketchCheckpointId::new(999);
7326
7327 let err = frontend
7328 .restore_sketch_checkpoint(missing_checkpoint)
7329 .await
7330 .expect_err("Expected restore to fail for missing checkpoint");
7331
7332 assert!(err.msg.contains("Sketch checkpoint not found"));
7333 }
7334
7335 #[tokio::test(flavor = "multi_thread")]
7336 async fn test_clear_sketch_checkpoints_removes_all_restore_points() {
7337 let mut frontend = FrontendState::new();
7338 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7339 let mock_ctx = ExecutorContext::new_mock(None).await;
7340 let version = Version(0);
7341
7342 let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
7343 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7344
7345 let checkpoint_a = frontend
7346 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7347 .await
7348 .unwrap();
7349 let checkpoint_b = frontend
7350 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7351 .await
7352 .unwrap();
7353 assert_eq!(frontend.sketch_checkpoints.len(), 2);
7354
7355 frontend.clear_sketch_checkpoints();
7356 assert!(frontend.sketch_checkpoints.is_empty());
7357 frontend.restore_sketch_checkpoint(checkpoint_a).await.unwrap_err();
7358 frontend.restore_sketch_checkpoint(checkpoint_b).await.unwrap_err();
7359
7360 ctx.close().await;
7361 }
7362
7363 #[tokio::test(flavor = "multi_thread")]
7364 async fn test_hack_set_program_keeps_old_checkpoints_and_adds_fresh_baseline() {
7365 let mut frontend = FrontendState::new();
7366 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7367 let mock_ctx = ExecutorContext::new_mock(None).await;
7368 let version = Version(0);
7369
7370 let (_sketch_id, _line_id, source_delta, scene_graph_delta) =
7371 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7372 let old_source = source_delta.text.clone();
7373 let old_checkpoint = frontend
7374 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7375 .await
7376 .unwrap();
7377 let initial_checkpoint_count = frontend.sketch_checkpoints.len();
7378
7379 let new_program = Program::parse("sketch(on = XY) {\n point(at = [1mm, 2mm])\n}\n")
7380 .unwrap()
7381 .0
7382 .unwrap();
7383
7384 let result = frontend.hack_set_program(&ctx, new_program).await.unwrap();
7385 let SetProgramOutcome::Success {
7386 checkpoint_id: Some(new_checkpoint),
7387 ..
7388 } = result
7389 else {
7390 panic!("Expected Success with a fresh checkpoint baseline");
7391 };
7392
7393 assert_eq!(frontend.sketch_checkpoints.len(), initial_checkpoint_count + 1);
7394
7395 let old_restore = frontend.restore_sketch_checkpoint(old_checkpoint).await.unwrap();
7396 assert_eq!(old_restore.source_delta.text, old_source);
7397
7398 let new_restore = frontend.restore_sketch_checkpoint(new_checkpoint).await.unwrap();
7399 assert!(new_restore.source_delta.text.contains("point(at = [1mm, 2mm])"));
7400
7401 ctx.close().await;
7402 }
7403
7404 #[tokio::test(flavor = "multi_thread")]
7405 async fn test_hack_set_program_exec_failure_does_not_add_checkpoint() {
7406 let mut frontend = FrontendState::new();
7407 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7408 let mock_ctx = ExecutorContext::new_mock(None).await;
7409 let version = Version(0);
7410
7411 let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
7412 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7413 let old_checkpoint = frontend
7414 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7415 .await
7416 .unwrap();
7417 let checkpoint_count_before = frontend.sketch_checkpoints.len();
7418
7419 let failing_program = Program::parse(
7420 "sketch(on = XY) {\n line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])\n}\n\nbad = missing_name\n",
7421 )
7422 .unwrap()
7423 .0
7424 .unwrap();
7425
7426 let result = frontend.hack_set_program(&ctx, failing_program).await.unwrap();
7427 assert!(matches!(result, SetProgramOutcome::ExecFailure { .. }));
7428 assert_eq!(frontend.sketch_checkpoints.len(), checkpoint_count_before);
7429 frontend.restore_sketch_checkpoint(old_checkpoint).await.unwrap();
7430
7431 ctx.close().await;
7432 }
7433
7434 #[tokio::test(flavor = "multi_thread")]
7435 async fn test_restore_sketch_checkpoint_restores_and_clears_mock_memory() {
7436 let mut frontend = FrontendState::new();
7437 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7438
7439 let program = Program::parse(
7440 "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",
7441 )
7442 .unwrap()
7443 .0
7444 .unwrap();
7445 let set_program_outcome = frontend.hack_set_program(&ctx, program).await.unwrap();
7446 let SetProgramOutcome::Success { exec_outcome, .. } = set_program_outcome else {
7447 panic!("Expected successful baseline program execution");
7448 };
7449
7450 clear_mem_cache().await;
7451 assert!(read_old_memory().await.is_none());
7452
7453 let checkpoint_without_mock_memory = frontend
7454 .create_sketch_checkpoint((*exec_outcome).clone())
7455 .await
7456 .unwrap();
7457
7458 write_old_memory(SketchModeState::new_for_tests()).await;
7459 assert!(read_old_memory().await.is_some());
7460
7461 let checkpoint_with_mock_memory = frontend
7462 .create_sketch_checkpoint((*exec_outcome).clone())
7463 .await
7464 .unwrap();
7465
7466 clear_mem_cache().await;
7467 assert!(read_old_memory().await.is_none());
7468
7469 frontend
7470 .restore_sketch_checkpoint(checkpoint_with_mock_memory)
7471 .await
7472 .unwrap();
7473 assert!(read_old_memory().await.is_some());
7474
7475 frontend
7476 .restore_sketch_checkpoint(checkpoint_without_mock_memory)
7477 .await
7478 .unwrap();
7479 assert!(read_old_memory().await.is_none());
7480
7481 ctx.close().await;
7482 }
7483
7484 #[tokio::test(flavor = "multi_thread")]
7485 async fn test_hack_set_program_exec_error_still_allows_edit_sketch() {
7486 let source = "\
7487sketch(on = XY) {
7488 line1 = line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])
7489}
7490
7491bad = missing_name
7492";
7493 let program = Program::parse(source).unwrap().0.unwrap();
7494
7495 let mut frontend = FrontendState::new();
7496
7497 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7498 let mock_ctx = ExecutorContext::new_mock(None).await;
7499 let version = Version(0);
7500 let project_id = ProjectId(0);
7501 let file_id = FileId(0);
7502
7503 let SetProgramOutcome::ExecFailure { .. } = frontend.hack_set_program(&ctx, program).await.unwrap() else {
7504 panic!("Expected ExecFailure from hack_set_program due to syntax error in program");
7505 };
7506
7507 let sketch_id = frontend
7508 .scene_graph
7509 .objects
7510 .iter()
7511 .find_map(|obj| matches!(obj.kind, ObjectKind::Sketch(_)).then_some(obj.id))
7512 .expect("Expected sketch object from errored hack_set_program");
7513
7514 frontend
7515 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
7516 .await
7517 .unwrap();
7518
7519 ctx.close().await;
7520 mock_ctx.close().await;
7521 }
7522
7523 #[tokio::test(flavor = "multi_thread")]
7524 async fn test_new_sketch_add_point_edit_point() {
7525 let program = Program::empty();
7526
7527 let mut frontend = FrontendState::new();
7528 frontend.program = program;
7529
7530 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7531 let mock_ctx = ExecutorContext::new_mock(None).await;
7532 let version = Version(0);
7533
7534 let sketch_args = SketchCtor {
7535 on: Plane::Default(PlaneName::Xy),
7536 };
7537 let (_src_delta, scene_delta, sketch_id) = frontend
7538 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
7539 .await
7540 .unwrap();
7541 assert_eq!(sketch_id, ObjectId(1));
7542 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
7543 let sketch_object = &scene_delta.new_graph.objects[1];
7544 assert_eq!(sketch_object.id, ObjectId(1));
7545 assert_eq!(
7546 sketch_object.kind,
7547 ObjectKind::Sketch(Sketch {
7548 args: SketchCtor {
7549 on: Plane::Default(PlaneName::Xy)
7550 },
7551 plane: ObjectId(0),
7552 segments: vec![],
7553 constraints: vec![],
7554 })
7555 );
7556 assert_eq!(scene_delta.new_graph.objects.len(), 2);
7557
7558 let point_ctor = PointCtor {
7559 position: Point2d {
7560 x: Expr::Number(Number {
7561 value: 1.0,
7562 units: NumericSuffix::Inch,
7563 }),
7564 y: Expr::Number(Number {
7565 value: 2.0,
7566 units: NumericSuffix::Inch,
7567 }),
7568 },
7569 };
7570 let segment = SegmentCtor::Point(point_ctor);
7571 let (src_delta, scene_delta) = frontend
7572 .add_segment(&mock_ctx, version, sketch_id, segment, None)
7573 .await
7574 .unwrap();
7575 assert_eq!(
7576 src_delta.text.as_str(),
7577 "sketch001 = sketch(on = XY) {
7578 point(at = [1in, 2in])
7579}
7580"
7581 );
7582 assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
7583 assert_eq!(scene_delta.new_graph.objects.len(), 3);
7584 for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
7585 assert_eq!(scene_object.id.0, i);
7586 }
7587
7588 let point_id = *scene_delta.new_objects.last().unwrap();
7589
7590 let point_ctor = PointCtor {
7591 position: Point2d {
7592 x: Expr::Number(Number {
7593 value: 3.0,
7594 units: NumericSuffix::Inch,
7595 }),
7596 y: Expr::Number(Number {
7597 value: 4.0,
7598 units: NumericSuffix::Inch,
7599 }),
7600 },
7601 };
7602 let segments = vec![ExistingSegmentCtor {
7603 id: point_id,
7604 ctor: SegmentCtor::Point(point_ctor),
7605 }];
7606 let (src_delta, scene_delta) = frontend
7607 .edit_segments(&mock_ctx, version, sketch_id, segments)
7608 .await
7609 .unwrap();
7610 assert_eq!(
7611 src_delta.text.as_str(),
7612 "sketch001 = sketch(on = XY) {
7613 point(at = [3in, 4in])
7614}
7615"
7616 );
7617 assert_eq!(scene_delta.new_objects, vec![]);
7618 assert_eq!(scene_delta.new_graph.objects.len(), 3);
7619
7620 ctx.close().await;
7621 mock_ctx.close().await;
7622 }
7623
7624 #[tokio::test(flavor = "multi_thread")]
7625 async fn test_new_sketch_add_line_edit_line() {
7626 let program = Program::empty();
7627
7628 let mut frontend = FrontendState::new();
7629 frontend.program = program;
7630
7631 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7632 let mock_ctx = ExecutorContext::new_mock(None).await;
7633 let version = Version(0);
7634
7635 let sketch_args = SketchCtor {
7636 on: Plane::Default(PlaneName::Xy),
7637 };
7638 let (_src_delta, scene_delta, sketch_id) = frontend
7639 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
7640 .await
7641 .unwrap();
7642 assert_eq!(sketch_id, ObjectId(1));
7643 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
7644 let sketch_object = &scene_delta.new_graph.objects[1];
7645 assert_eq!(sketch_object.id, ObjectId(1));
7646 assert_eq!(
7647 sketch_object.kind,
7648 ObjectKind::Sketch(Sketch {
7649 args: SketchCtor {
7650 on: Plane::Default(PlaneName::Xy)
7651 },
7652 plane: ObjectId(0),
7653 segments: vec![],
7654 constraints: vec![],
7655 })
7656 );
7657 assert_eq!(scene_delta.new_graph.objects.len(), 2);
7658
7659 let line_ctor = LineCtor {
7660 start: Point2d {
7661 x: Expr::Number(Number {
7662 value: 0.0,
7663 units: NumericSuffix::Mm,
7664 }),
7665 y: Expr::Number(Number {
7666 value: 0.0,
7667 units: NumericSuffix::Mm,
7668 }),
7669 },
7670 end: Point2d {
7671 x: Expr::Number(Number {
7672 value: 10.0,
7673 units: NumericSuffix::Mm,
7674 }),
7675 y: Expr::Number(Number {
7676 value: 10.0,
7677 units: NumericSuffix::Mm,
7678 }),
7679 },
7680 construction: None,
7681 };
7682 let segment = SegmentCtor::Line(line_ctor);
7683 let (src_delta, scene_delta) = frontend
7684 .add_segment(&mock_ctx, version, sketch_id, segment, None)
7685 .await
7686 .unwrap();
7687 assert_eq!(
7688 src_delta.text.as_str(),
7689 "sketch001 = sketch(on = XY) {
7690 line(start = [0mm, 0mm], end = [10mm, 10mm])
7691}
7692"
7693 );
7694 assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
7695 assert_eq!(scene_delta.new_graph.objects.len(), 5);
7696 for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
7697 assert_eq!(scene_object.id.0, i);
7698 }
7699
7700 let line = *scene_delta.new_objects.last().unwrap();
7702
7703 let line_ctor = LineCtor {
7704 start: Point2d {
7705 x: Expr::Number(Number {
7706 value: 1.0,
7707 units: NumericSuffix::Mm,
7708 }),
7709 y: Expr::Number(Number {
7710 value: 2.0,
7711 units: NumericSuffix::Mm,
7712 }),
7713 },
7714 end: Point2d {
7715 x: Expr::Number(Number {
7716 value: 13.0,
7717 units: NumericSuffix::Mm,
7718 }),
7719 y: Expr::Number(Number {
7720 value: 14.0,
7721 units: NumericSuffix::Mm,
7722 }),
7723 },
7724 construction: None,
7725 };
7726 let segments = vec![ExistingSegmentCtor {
7727 id: line,
7728 ctor: SegmentCtor::Line(line_ctor),
7729 }];
7730 let (src_delta, scene_delta) = frontend
7731 .edit_segments(&mock_ctx, version, sketch_id, segments)
7732 .await
7733 .unwrap();
7734 assert_eq!(
7735 src_delta.text.as_str(),
7736 "sketch001 = sketch(on = XY) {
7737 line(start = [1mm, 2mm], end = [13mm, 14mm])
7738}
7739"
7740 );
7741 assert_eq!(scene_delta.new_objects, vec![]);
7742 assert_eq!(scene_delta.new_graph.objects.len(), 5);
7743
7744 ctx.close().await;
7745 mock_ctx.close().await;
7746 }
7747
7748 #[tokio::test(flavor = "multi_thread")]
7749 async fn test_new_sketch_add_arc_edit_arc() {
7750 let program = Program::empty();
7751
7752 let mut frontend = FrontendState::new();
7753 frontend.program = program;
7754
7755 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7756 let mock_ctx = ExecutorContext::new_mock(None).await;
7757 let version = Version(0);
7758
7759 let sketch_args = SketchCtor {
7760 on: Plane::Default(PlaneName::Xy),
7761 };
7762 let (_src_delta, scene_delta, sketch_id) = frontend
7763 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
7764 .await
7765 .unwrap();
7766 assert_eq!(sketch_id, ObjectId(1));
7767 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
7768 let sketch_object = &scene_delta.new_graph.objects[1];
7769 assert_eq!(sketch_object.id, ObjectId(1));
7770 assert_eq!(
7771 sketch_object.kind,
7772 ObjectKind::Sketch(Sketch {
7773 args: SketchCtor {
7774 on: Plane::Default(PlaneName::Xy),
7775 },
7776 plane: ObjectId(0),
7777 segments: vec![],
7778 constraints: vec![],
7779 })
7780 );
7781 assert_eq!(scene_delta.new_graph.objects.len(), 2);
7782
7783 let arc_ctor = ArcCtor {
7784 start: Point2d {
7785 x: Expr::Var(Number {
7786 value: 0.0,
7787 units: NumericSuffix::Mm,
7788 }),
7789 y: Expr::Var(Number {
7790 value: 0.0,
7791 units: NumericSuffix::Mm,
7792 }),
7793 },
7794 end: Point2d {
7795 x: Expr::Var(Number {
7796 value: 10.0,
7797 units: NumericSuffix::Mm,
7798 }),
7799 y: Expr::Var(Number {
7800 value: 10.0,
7801 units: NumericSuffix::Mm,
7802 }),
7803 },
7804 center: Point2d {
7805 x: Expr::Var(Number {
7806 value: 10.0,
7807 units: NumericSuffix::Mm,
7808 }),
7809 y: Expr::Var(Number {
7810 value: 0.0,
7811 units: NumericSuffix::Mm,
7812 }),
7813 },
7814 construction: None,
7815 };
7816 let segment = SegmentCtor::Arc(arc_ctor);
7817 let (src_delta, scene_delta) = frontend
7818 .add_segment(&mock_ctx, version, sketch_id, segment, None)
7819 .await
7820 .unwrap();
7821 assert_eq!(
7822 src_delta.text.as_str(),
7823 "sketch001 = sketch(on = XY) {
7824 arc(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm], center = [var 10mm, var 0mm])
7825}
7826"
7827 );
7828 assert_eq!(
7829 scene_delta.new_objects,
7830 vec![ObjectId(2), ObjectId(3), ObjectId(4), ObjectId(5)]
7831 );
7832 for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
7833 assert_eq!(scene_object.id.0, i);
7834 }
7835 assert_eq!(scene_delta.new_graph.objects.len(), 6);
7836
7837 let arc = *scene_delta.new_objects.last().unwrap();
7839
7840 let arc_ctor = ArcCtor {
7841 start: Point2d {
7842 x: Expr::Var(Number {
7843 value: 1.0,
7844 units: NumericSuffix::Mm,
7845 }),
7846 y: Expr::Var(Number {
7847 value: 2.0,
7848 units: NumericSuffix::Mm,
7849 }),
7850 },
7851 end: Point2d {
7852 x: Expr::Var(Number {
7853 value: 13.0,
7854 units: NumericSuffix::Mm,
7855 }),
7856 y: Expr::Var(Number {
7857 value: 14.0,
7858 units: NumericSuffix::Mm,
7859 }),
7860 },
7861 center: Point2d {
7862 x: Expr::Var(Number {
7863 value: 13.0,
7864 units: NumericSuffix::Mm,
7865 }),
7866 y: Expr::Var(Number {
7867 value: 2.0,
7868 units: NumericSuffix::Mm,
7869 }),
7870 },
7871 construction: None,
7872 };
7873 let segments = vec![ExistingSegmentCtor {
7874 id: arc,
7875 ctor: SegmentCtor::Arc(arc_ctor),
7876 }];
7877 let (src_delta, scene_delta) = frontend
7878 .edit_segments(&mock_ctx, version, sketch_id, segments)
7879 .await
7880 .unwrap();
7881 assert_eq!(
7882 src_delta.text.as_str(),
7883 "sketch001 = sketch(on = XY) {
7884 arc(start = [var 1mm, var 2mm], end = [var 13mm, var 14mm], center = [var 13mm, var 2mm])
7885}
7886"
7887 );
7888 assert_eq!(scene_delta.new_objects, vec![]);
7889 assert_eq!(scene_delta.new_graph.objects.len(), 6);
7890
7891 ctx.close().await;
7892 mock_ctx.close().await;
7893 }
7894
7895 #[tokio::test(flavor = "multi_thread")]
7896 async fn test_new_sketch_add_circle_edit_circle() {
7897 let program = Program::empty();
7898
7899 let mut frontend = FrontendState::new();
7900 frontend.program = program;
7901
7902 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7903 let mock_ctx = ExecutorContext::new_mock(None).await;
7904 let version = Version(0);
7905
7906 let sketch_args = SketchCtor {
7907 on: Plane::Default(PlaneName::Xy),
7908 };
7909 let (_src_delta, _scene_delta, sketch_id) = frontend
7910 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
7911 .await
7912 .unwrap();
7913
7914 let circle_ctor = CircleCtor {
7916 start: Point2d {
7917 x: Expr::Var(Number {
7918 value: 5.0,
7919 units: NumericSuffix::Mm,
7920 }),
7921 y: Expr::Var(Number {
7922 value: 0.0,
7923 units: NumericSuffix::Mm,
7924 }),
7925 },
7926 center: Point2d {
7927 x: Expr::Var(Number {
7928 value: 0.0,
7929 units: NumericSuffix::Mm,
7930 }),
7931 y: Expr::Var(Number {
7932 value: 0.0,
7933 units: NumericSuffix::Mm,
7934 }),
7935 },
7936 construction: None,
7937 };
7938 let segment = SegmentCtor::Circle(circle_ctor);
7939 let (src_delta, scene_delta) = frontend
7940 .add_segment(&mock_ctx, version, sketch_id, segment, None)
7941 .await
7942 .unwrap();
7943 assert_eq!(
7944 src_delta.text.as_str(),
7945 "sketch001 = sketch(on = XY) {
7946 circle1 = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
7947}
7948"
7949 );
7950 assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
7952 assert_eq!(scene_delta.new_graph.objects.len(), 5);
7953
7954 let circle = *scene_delta.new_objects.last().unwrap();
7955
7956 let circle_ctor = CircleCtor {
7958 start: Point2d {
7959 x: Expr::Var(Number {
7960 value: 10.0,
7961 units: NumericSuffix::Mm,
7962 }),
7963 y: Expr::Var(Number {
7964 value: 0.0,
7965 units: NumericSuffix::Mm,
7966 }),
7967 },
7968 center: Point2d {
7969 x: Expr::Var(Number {
7970 value: 3.0,
7971 units: NumericSuffix::Mm,
7972 }),
7973 y: Expr::Var(Number {
7974 value: 4.0,
7975 units: NumericSuffix::Mm,
7976 }),
7977 },
7978 construction: None,
7979 };
7980 let segments = vec![ExistingSegmentCtor {
7981 id: circle,
7982 ctor: SegmentCtor::Circle(circle_ctor),
7983 }];
7984 let (src_delta, scene_delta) = frontend
7985 .edit_segments(&mock_ctx, version, sketch_id, segments)
7986 .await
7987 .unwrap();
7988 assert_eq!(
7989 src_delta.text.as_str(),
7990 "sketch001 = sketch(on = XY) {
7991 circle1 = circle(start = [var 10mm, var 0mm], center = [var 3mm, var 4mm])
7992}
7993"
7994 );
7995 assert_eq!(scene_delta.new_objects, vec![]);
7996 assert_eq!(scene_delta.new_graph.objects.len(), 5);
7997
7998 ctx.close().await;
7999 mock_ctx.close().await;
8000 }
8001
8002 #[tokio::test(flavor = "multi_thread")]
8003 async fn test_delete_circle() {
8004 let initial_source = "sketch001 = sketch(on = XY) {
8005 circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
8006}
8007";
8008
8009 let program = Program::parse(initial_source).unwrap().0.unwrap();
8010 let mut frontend = FrontendState::new();
8011
8012 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8013 let mock_ctx = ExecutorContext::new_mock(None).await;
8014 let version = Version(0);
8015
8016 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8017 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8018 let sketch_id = sketch_object.id;
8019 let sketch = expect_sketch(sketch_object);
8020
8021 assert_eq!(sketch.segments.len(), 3);
8023 let circle_id = sketch.segments[2];
8024
8025 let (src_delta, scene_delta) = frontend
8027 .delete_objects(&mock_ctx, version, sketch_id, vec![], vec![circle_id])
8028 .await
8029 .unwrap();
8030 assert_eq!(
8031 src_delta.text.as_str(),
8032 "sketch001 = sketch(on = XY) {
8033}
8034"
8035 );
8036 let new_sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8037 let new_sketch = expect_sketch(new_sketch_object);
8038 assert_eq!(new_sketch.segments.len(), 0);
8039
8040 ctx.close().await;
8041 mock_ctx.close().await;
8042 }
8043
8044 #[tokio::test(flavor = "multi_thread")]
8045 async fn test_edit_circle_via_point() {
8046 let initial_source = "sketch001 = sketch(on = XY) {
8047 circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
8048}
8049";
8050
8051 let program = Program::parse(initial_source).unwrap().0.unwrap();
8052 let mut frontend = FrontendState::new();
8053
8054 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8055 let mock_ctx = ExecutorContext::new_mock(None).await;
8056 let version = Version(0);
8057
8058 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8059 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8060 let sketch_id = sketch_object.id;
8061 let sketch = expect_sketch(sketch_object);
8062
8063 let circle_id = sketch
8065 .segments
8066 .iter()
8067 .copied()
8068 .find(|seg_id| {
8069 matches!(
8070 &frontend.scene_graph.objects[seg_id.0].kind,
8071 ObjectKind::Segment {
8072 segment: Segment::Circle(_)
8073 }
8074 )
8075 })
8076 .expect("Expected a circle segment in sketch");
8077 let circle_object = &frontend.scene_graph.objects[circle_id.0];
8078 let ObjectKind::Segment {
8079 segment: Segment::Circle(circle),
8080 } = &circle_object.kind
8081 else {
8082 panic!("Expected circle segment, got: {:?}", circle_object.kind);
8083 };
8084 let start_point_id = circle.start;
8085
8086 let segments = vec![ExistingSegmentCtor {
8088 id: start_point_id,
8089 ctor: SegmentCtor::Point(PointCtor {
8090 position: Point2d {
8091 x: Expr::Var(Number {
8092 value: 7.0,
8093 units: NumericSuffix::Mm,
8094 }),
8095 y: Expr::Var(Number {
8096 value: 1.0,
8097 units: NumericSuffix::Mm,
8098 }),
8099 },
8100 }),
8101 }];
8102 let (src_delta, _scene_delta) = frontend
8103 .edit_segments(&mock_ctx, version, sketch_id, segments)
8104 .await
8105 .unwrap();
8106 assert_eq!(
8107 src_delta.text.as_str(),
8108 "sketch001 = sketch(on = XY) {
8109 circle(start = [var 7mm, var 1mm], center = [var 0mm, var 0mm])
8110}
8111"
8112 );
8113
8114 ctx.close().await;
8115 mock_ctx.close().await;
8116 }
8117
8118 #[tokio::test(flavor = "multi_thread")]
8119 async fn test_add_line_when_sketch_block_uses_variable() {
8120 let initial_source = "s = sketch(on = XY) {}
8121";
8122
8123 let program = Program::parse(initial_source).unwrap().0.unwrap();
8124
8125 let mut frontend = FrontendState::new();
8126
8127 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8128 let mock_ctx = ExecutorContext::new_mock(None).await;
8129 let version = Version(0);
8130
8131 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8132 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8133 let sketch_id = sketch_object.id;
8134
8135 let line_ctor = LineCtor {
8136 start: Point2d {
8137 x: Expr::Number(Number {
8138 value: 0.0,
8139 units: NumericSuffix::Mm,
8140 }),
8141 y: Expr::Number(Number {
8142 value: 0.0,
8143 units: NumericSuffix::Mm,
8144 }),
8145 },
8146 end: Point2d {
8147 x: Expr::Number(Number {
8148 value: 10.0,
8149 units: NumericSuffix::Mm,
8150 }),
8151 y: Expr::Number(Number {
8152 value: 10.0,
8153 units: NumericSuffix::Mm,
8154 }),
8155 },
8156 construction: None,
8157 };
8158 let segment = SegmentCtor::Line(line_ctor);
8159 let (src_delta, scene_delta) = frontend
8160 .add_segment(&mock_ctx, version, sketch_id, segment, None)
8161 .await
8162 .unwrap();
8163 assert_eq!(
8164 src_delta.text.as_str(),
8165 "s = sketch(on = XY) {
8166 line(start = [0mm, 0mm], end = [10mm, 10mm])
8167}
8168"
8169 );
8170 assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
8171 assert_eq!(scene_delta.new_graph.objects.len(), 5);
8172
8173 ctx.close().await;
8174 mock_ctx.close().await;
8175 }
8176
8177 #[tokio::test(flavor = "multi_thread")]
8178 async fn test_new_sketch_add_line_delete_sketch() {
8179 let program = Program::empty();
8180
8181 let mut frontend = FrontendState::new();
8182 frontend.program = program;
8183
8184 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8185 let mock_ctx = ExecutorContext::new_mock(None).await;
8186 let version = Version(0);
8187
8188 let sketch_args = SketchCtor {
8189 on: Plane::Default(PlaneName::Xy),
8190 };
8191 let (_src_delta, scene_delta, sketch_id) = frontend
8192 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
8193 .await
8194 .unwrap();
8195 assert_eq!(sketch_id, ObjectId(1));
8196 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
8197 let sketch_object = &scene_delta.new_graph.objects[1];
8198 assert_eq!(sketch_object.id, ObjectId(1));
8199 assert_eq!(
8200 sketch_object.kind,
8201 ObjectKind::Sketch(Sketch {
8202 args: SketchCtor {
8203 on: Plane::Default(PlaneName::Xy)
8204 },
8205 plane: ObjectId(0),
8206 segments: vec![],
8207 constraints: vec![],
8208 })
8209 );
8210 assert_eq!(scene_delta.new_graph.objects.len(), 2);
8211
8212 let line_ctor = LineCtor {
8213 start: Point2d {
8214 x: Expr::Number(Number {
8215 value: 0.0,
8216 units: NumericSuffix::Mm,
8217 }),
8218 y: Expr::Number(Number {
8219 value: 0.0,
8220 units: NumericSuffix::Mm,
8221 }),
8222 },
8223 end: Point2d {
8224 x: Expr::Number(Number {
8225 value: 10.0,
8226 units: NumericSuffix::Mm,
8227 }),
8228 y: Expr::Number(Number {
8229 value: 10.0,
8230 units: NumericSuffix::Mm,
8231 }),
8232 },
8233 construction: None,
8234 };
8235 let segment = SegmentCtor::Line(line_ctor);
8236 let (src_delta, scene_delta) = frontend
8237 .add_segment(&mock_ctx, version, sketch_id, segment, None)
8238 .await
8239 .unwrap();
8240 assert_eq!(
8241 src_delta.text.as_str(),
8242 "sketch001 = sketch(on = XY) {
8243 line(start = [0mm, 0mm], end = [10mm, 10mm])
8244}
8245"
8246 );
8247 assert_eq!(scene_delta.new_graph.objects.len(), 5);
8248
8249 let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8250 assert_eq!(src_delta.text.as_str(), "");
8251 assert_eq!(scene_delta.new_graph.objects.len(), 0);
8252
8253 ctx.close().await;
8254 mock_ctx.close().await;
8255 }
8256
8257 #[tokio::test(flavor = "multi_thread")]
8258 async fn test_delete_sketch_when_sketch_block_uses_variable() {
8259 let initial_source = "s = sketch(on = XY) {}
8260";
8261
8262 let program = Program::parse(initial_source).unwrap().0.unwrap();
8263
8264 let mut frontend = FrontendState::new();
8265
8266 let ctx = ExecutorContext::new_with_engine(sync::Arc::new(EngineManager::new_mock()), Default::default());
8267 let version = Version(0);
8268
8269 frontend.hack_set_program(&ctx, program).await.unwrap();
8270 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8271 let sketch_id = sketch_object.id;
8272
8273 let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8274 assert_eq!(src_delta.text.as_str(), "");
8275 assert_eq!(scene_delta.new_graph.objects.len(), 0);
8276
8277 ctx.close().await;
8278 }
8279
8280 #[tokio::test(flavor = "multi_thread")]
8281 async fn test_delete_sketch_after_comment() {
8282 let initial_source = "sketch001 = sketch(on = XZ) {
8283}
8284";
8285
8286 let program = Program::parse(initial_source).unwrap().0.unwrap();
8287 let mut frontend = FrontendState::new();
8288
8289 let ctx = ExecutorContext::new_with_engine(sync::Arc::new(EngineManager::new_mock()), Default::default());
8290 let version = Version(0);
8291
8292 frontend.hack_set_program(&ctx, program).await.unwrap();
8293 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8294 let sketch_id = sketch_object.id;
8295 let original_source = sketch_object.source.clone();
8296
8297 let commented_source = "// test 1
8298sketch001 = sketch(on = XZ) {
8299}
8300";
8301 let commented_program = Program::parse(commented_source).unwrap().0.unwrap();
8302 frontend.engine_execute(&ctx, commented_program).await.unwrap();
8303
8304 let cached_sketch_object = &frontend.scene_graph.objects[sketch_id.0];
8305 assert_eq!(cached_sketch_object.source, original_source);
8306
8307 let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8308 assert!(
8309 !src_delta.text.contains("sketch001"),
8310 "sketch was not deleted: {}",
8311 src_delta.text
8312 );
8313 assert_eq!(src_delta.text.as_str(), "// test 1\n");
8315 assert_eq!(scene_delta.new_graph.objects.len(), 0);
8316
8317 ctx.close().await;
8318 }
8319
8320 #[tokio::test(flavor = "multi_thread")]
8321 async fn test_delete_sketch_preserves_pre_comment_when_followed_by_code() {
8322 let initial_source = "sketch001 = sketch(on = XZ) {
8323}
8324foo = 1
8325";
8326
8327 let program = Program::parse(initial_source).unwrap().0.unwrap();
8328 let mut frontend = FrontendState::new();
8329
8330 let ctx = ExecutorContext::new_with_engine(sync::Arc::new(EngineManager::new_mock()), Default::default());
8331 let version = Version(0);
8332
8333 frontend.hack_set_program(&ctx, program).await.unwrap();
8334 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8335 let sketch_id = sketch_object.id;
8336
8337 let commented_source = "// keep me
8338sketch001 = sketch(on = XZ) {
8339}
8340foo = 1
8341";
8342 let commented_program = Program::parse(commented_source).unwrap().0.unwrap();
8343 frontend.engine_execute(&ctx, commented_program).await.unwrap();
8344
8345 let (src_delta, _scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8346 assert_eq!(src_delta.text.as_str(), "// keep me\nfoo = 1\n");
8348
8349 ctx.close().await;
8350 }
8351
8352 #[tokio::test(flavor = "multi_thread")]
8353 async fn test_delete_segment_preserves_pre_comment() {
8354 let initial_source = "\
8355sketch(on = XY) {
8356 point(at = [var 1, var 2])
8357 // describe the middle point
8358 point(at = [var 3, var 4])
8359 point(at = [var 5, var 6])
8360}
8361";
8362
8363 let program = Program::parse(initial_source).unwrap().0.unwrap();
8364 let mut frontend = FrontendState::new();
8365
8366 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8367 let mock_ctx = ExecutorContext::new_mock(None).await;
8368 let version = Version(0);
8369
8370 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8371 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8372 let sketch_id = sketch_object.id;
8373 let sketch = expect_sketch(sketch_object);
8374
8375 let middle_point_id = *sketch.segments.get(1).unwrap();
8376
8377 let (src_delta, _scene_delta) = frontend
8378 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![middle_point_id])
8379 .await
8380 .unwrap();
8381 assert_eq!(
8384 src_delta.text.as_str(),
8385 "\
8386sketch(on = XY) {
8387 point(at = [var 1, var 2])
8388 // describe the middle point
8389 point(at = [var 5, var 6])
8390}
8391"
8392 );
8393
8394 ctx.close().await;
8395 mock_ctx.close().await;
8396 }
8397
8398 #[tokio::test(flavor = "multi_thread")]
8399 async fn test_delete_last_segment_preserves_pre_comment() {
8400 let initial_source = "\
8401sketch(on = XY) {
8402 point(at = [var 1, var 2])
8403 // describe the trailing point
8404 point(at = [var 3, var 4])
8405}
8406";
8407
8408 let program = Program::parse(initial_source).unwrap().0.unwrap();
8409 let mut frontend = FrontendState::new();
8410
8411 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8412 let mock_ctx = ExecutorContext::new_mock(None).await;
8413 let version = Version(0);
8414
8415 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8416 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8417 let sketch_id = sketch_object.id;
8418 let sketch = expect_sketch(sketch_object);
8419
8420 let last_point_id = *sketch.segments.last().unwrap();
8421
8422 let (src_delta, _scene_delta) = frontend
8423 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![last_point_id])
8424 .await
8425 .unwrap();
8426 assert_eq!(
8429 src_delta.text.as_str(),
8430 "\
8431sketch(on = XY) {
8432 point(at = [var 1, var 2])
8433 // describe the trailing point
8434}
8435"
8436 );
8437
8438 ctx.close().await;
8439 mock_ctx.close().await;
8440 }
8441
8442 #[tokio::test(flavor = "multi_thread")]
8443 async fn test_delete_segment_drops_inline_trailing_comment() {
8444 let initial_source = "\
8445sketch(on = XY) {
8446 point(at = [var 1, var 2])
8447 point(at = [var 3, var 4]) // same-line note that gets dropped
8448 point(at = [var 5, var 6])
8449}
8450";
8451
8452 let program = Program::parse(initial_source).unwrap().0.unwrap();
8453 let mut frontend = FrontendState::new();
8454
8455 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8456 let mock_ctx = ExecutorContext::new_mock(None).await;
8457 let version = Version(0);
8458
8459 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8460 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8461 let sketch_id = sketch_object.id;
8462 let sketch = expect_sketch(sketch_object);
8463
8464 let middle_point_id = *sketch.segments.get(1).unwrap();
8465
8466 let (src_delta, _scene_delta) = frontend
8467 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![middle_point_id])
8468 .await
8469 .unwrap();
8470 assert!(
8472 !src_delta.text.contains("same-line note"),
8473 "inline comment should have been removed: {}",
8474 src_delta.text
8475 );
8476
8477 ctx.close().await;
8478 mock_ctx.close().await;
8479 }
8480
8481 #[tokio::test(flavor = "multi_thread")]
8482 async fn test_delete_segments_preserves_block_comments_across_positions() {
8483 let initial_source = "\
8491sketch(on = XY) {
8492 /* above first - moves to middle */
8493 point(at = [var 1, var 2]) /* same-line on first - dropped */
8494 /* above middle - stays */
8495 point(at = [var 3, var 4])
8496 /* above last - moves to trailing meta */
8497 point(at = [var 5, var 6])
8498}
8499";
8500
8501 let program = Program::parse(initial_source).unwrap().0.unwrap();
8502 let mut frontend = FrontendState::new();
8503
8504 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8505 let mock_ctx = ExecutorContext::new_mock(None).await;
8506 let version = Version(0);
8507
8508 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8509 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8510 let sketch_id = sketch_object.id;
8511 let sketch = expect_sketch(sketch_object);
8512
8513 let first_point_id = *sketch.segments.first().unwrap();
8514 let last_point_id = *sketch.segments.last().unwrap();
8515
8516 let (src_delta, _scene_delta) = frontend
8517 .delete_objects(
8518 &mock_ctx,
8519 version,
8520 sketch_id,
8521 Vec::new(),
8522 vec![first_point_id, last_point_id],
8523 )
8524 .await
8525 .unwrap();
8526 assert_eq!(
8527 src_delta.text.as_str(),
8528 "\
8529sketch(on = XY) {
8530 /* above first - moves to middle */
8531 /* above middle - stays */
8532 point(at = [var 3, var 4])
8533 /* above last - moves to trailing meta */
8534}
8535"
8536 );
8537
8538 ctx.close().await;
8539 mock_ctx.close().await;
8540 }
8541
8542 #[tokio::test(flavor = "multi_thread")]
8543 async fn test_edit_line_when_editing_its_start_point() {
8544 let initial_source = "\
8545sketch(on = XY) {
8546 line(start = [var 1, var 2], end = [var 3, var 4])
8547}
8548";
8549
8550 let program = Program::parse(initial_source).unwrap().0.unwrap();
8551
8552 let mut frontend = FrontendState::new();
8553
8554 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8555 let mock_ctx = ExecutorContext::new_mock(None).await;
8556 let version = Version(0);
8557
8558 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8559 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8560 let sketch_id = sketch_object.id;
8561 let sketch = expect_sketch(sketch_object);
8562
8563 let point_id = *sketch.segments.first().unwrap();
8564
8565 let point_ctor = PointCtor {
8566 position: Point2d {
8567 x: Expr::Var(Number {
8568 value: 5.0,
8569 units: NumericSuffix::Inch,
8570 }),
8571 y: Expr::Var(Number {
8572 value: 6.0,
8573 units: NumericSuffix::Inch,
8574 }),
8575 },
8576 };
8577 let segments = vec![ExistingSegmentCtor {
8578 id: point_id,
8579 ctor: SegmentCtor::Point(point_ctor),
8580 }];
8581 let (src_delta, scene_delta) = frontend
8582 .edit_segments(&mock_ctx, version, sketch_id, segments)
8583 .await
8584 .unwrap();
8585 assert_eq!(
8586 src_delta.text.as_str(),
8587 "\
8588sketch(on = XY) {
8589 line(start = [var 5in, var 6in], end = [var 3, var 4])
8590}
8591"
8592 );
8593 assert_eq!(scene_delta.new_objects, vec![]);
8594 assert_eq!(scene_delta.new_graph.objects.len(), 5);
8595
8596 ctx.close().await;
8597 mock_ctx.close().await;
8598 }
8599
8600 #[tokio::test(flavor = "multi_thread")]
8601 async fn test_edit_line_when_editing_its_end_point() {
8602 let initial_source = "\
8603sketch(on = XY) {
8604 line(start = [var 1, var 2], end = [var 3, var 4])
8605}
8606";
8607
8608 let program = Program::parse(initial_source).unwrap().0.unwrap();
8609
8610 let mut frontend = FrontendState::new();
8611
8612 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8613 let mock_ctx = ExecutorContext::new_mock(None).await;
8614 let version = Version(0);
8615
8616 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8617 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8618 let sketch_id = sketch_object.id;
8619 let sketch = expect_sketch(sketch_object);
8620 let point_id = *sketch.segments.get(1).unwrap();
8621
8622 let point_ctor = PointCtor {
8623 position: Point2d {
8624 x: Expr::Var(Number {
8625 value: 5.0,
8626 units: NumericSuffix::Inch,
8627 }),
8628 y: Expr::Var(Number {
8629 value: 6.0,
8630 units: NumericSuffix::Inch,
8631 }),
8632 },
8633 };
8634 let segments = vec![ExistingSegmentCtor {
8635 id: point_id,
8636 ctor: SegmentCtor::Point(point_ctor),
8637 }];
8638 let (src_delta, scene_delta) = frontend
8639 .edit_segments(&mock_ctx, version, sketch_id, segments)
8640 .await
8641 .unwrap();
8642 assert_eq!(
8643 src_delta.text.as_str(),
8644 "\
8645sketch(on = XY) {
8646 line(start = [var 1, var 2], end = [var 5in, var 6in])
8647}
8648"
8649 );
8650 assert_eq!(scene_delta.new_objects, vec![]);
8651 assert_eq!(
8652 scene_delta.new_graph.objects.len(),
8653 5,
8654 "{:#?}",
8655 scene_delta.new_graph.objects
8656 );
8657
8658 ctx.close().await;
8659 mock_ctx.close().await;
8660 }
8661
8662 #[tokio::test(flavor = "multi_thread")]
8663 async fn test_edit_line_with_coincident_feedback() {
8664 let initial_source = "\
8665sketch(on = XY) {
8666 line1 = line(start = [var 1, var 2], end = [var 1, var 2])
8667 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8668 fixed([line1.start, [0, 0]])
8669 coincident([line1.end, line2.start])
8670 equalLength([line1, line2])
8671}
8672";
8673
8674 let program = Program::parse(initial_source).unwrap().0.unwrap();
8675
8676 let mut frontend = FrontendState::new();
8677
8678 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8679 let mock_ctx = ExecutorContext::new_mock(None).await;
8680 let version = Version(0);
8681
8682 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8683 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8684 let sketch_id = sketch_object.id;
8685 let sketch = expect_sketch(sketch_object);
8686 let line2_end_id = *sketch.segments.get(4).unwrap();
8687
8688 let segments = vec![ExistingSegmentCtor {
8689 id: line2_end_id,
8690 ctor: SegmentCtor::Point(PointCtor {
8691 position: Point2d {
8692 x: Expr::Var(Number {
8693 value: 9.0,
8694 units: NumericSuffix::None,
8695 }),
8696 y: Expr::Var(Number {
8697 value: 10.0,
8698 units: NumericSuffix::None,
8699 }),
8700 },
8701 }),
8702 }];
8703 let (src_delta, scene_delta) = frontend
8704 .edit_segments(&mock_ctx, version, sketch_id, segments)
8705 .await
8706 .unwrap();
8707 assert_eq!(
8708 src_delta.text.as_str(),
8709 "\
8710sketch(on = XY) {
8711 line1 = line(start = [var 0, var 0], end = [var 4.14, var 5.32])
8712 line2 = line(start = [var 4.14, var 5.32], end = [var 9, var 10])
8713 fixed([line1.start, [0, 0]])
8714 coincident([line1.end, line2.start])
8715 equalLength([line1, line2])
8716}
8717"
8718 );
8719 assert_eq!(
8720 scene_delta.new_graph.objects.len(),
8721 11,
8722 "{:#?}",
8723 scene_delta.new_graph.objects
8724 );
8725
8726 ctx.close().await;
8727 mock_ctx.close().await;
8728 }
8729
8730 #[tokio::test(flavor = "multi_thread")]
8731 async fn test_edit_segments_persists_solver_feedback_for_next_mock_execute() {
8732 let initial_source = "\
8733sketch(on = XY) {
8734 line1 = line(start = [var 1, var 2], end = [var 1, var 2])
8735 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8736 fixed([line1.start, [0, 0]])
8737 coincident([line1.end, line2.start])
8738 equalLength([line1, line2])
8739}
8740";
8741
8742 let program = Program::parse(initial_source).unwrap().0.unwrap();
8743 let mut frontend = FrontendState::new();
8744 let mock_ctx = ExecutorContext::new_mock(None).await;
8745 let version = Version(0);
8746
8747 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8748 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8749 let sketch_id = sketch_object.id;
8750 let sketch = expect_sketch(sketch_object);
8751 let line2_end_id = *sketch.segments.get(4).unwrap();
8752
8753 let segments = vec![ExistingSegmentCtor {
8754 id: line2_end_id,
8755 ctor: SegmentCtor::Point(PointCtor {
8756 position: Point2d {
8757 x: Expr::Var(Number {
8758 value: 9.0,
8759 units: NumericSuffix::None,
8760 }),
8761 y: Expr::Var(Number {
8762 value: 10.0,
8763 units: NumericSuffix::None,
8764 }),
8765 },
8766 }),
8767 }];
8768 let (edited_source, _) = frontend
8769 .edit_segments(&mock_ctx, version, sketch_id, segments)
8770 .await
8771 .unwrap();
8772
8773 let (mock_source, _) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
8774 assert_eq!(mock_source.text, edited_source.text);
8775
8776 mock_ctx.close().await;
8777 }
8778
8779 #[tokio::test(flavor = "multi_thread")]
8782 async fn test_preview_edit_segments_does_not_persist_solver_feedback() {
8783 let initial_source = "\
8784sketch(on = XY) {
8785 line1 = line(start = [var 1, var 2], end = [var 1, var 2])
8786 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8787 fixed([line1.start, [0, 0]])
8788 coincident([line1.end, line2.start])
8789 equalLength([line1, line2])
8790}
8791";
8792
8793 let program = Program::parse(initial_source).unwrap().0.unwrap();
8794 let mut frontend = FrontendState::new();
8795 let mock_ctx = ExecutorContext::new_mock(None).await;
8796 let version = Version(0);
8797
8798 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8799 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8800 let sketch_id = sketch_object.id;
8801 let sketch = expect_sketch(sketch_object);
8802 let line2_end_id = *sketch.segments.get(4).unwrap();
8803
8804 let segments = vec![ExistingSegmentCtor {
8805 id: line2_end_id,
8806 ctor: SegmentCtor::Point(PointCtor {
8807 position: Point2d {
8808 x: Expr::Var(Number {
8809 value: 9.0,
8810 units: NumericSuffix::None,
8811 }),
8812 y: Expr::Var(Number {
8813 value: 10.0,
8814 units: NumericSuffix::None,
8815 }),
8816 },
8817 }),
8818 }];
8819 let (preview_source, preview_delta) = frontend
8820 .edit_segments_with_options(
8821 &mock_ctx,
8822 version,
8823 sketch_id,
8824 segments,
8825 EditSegmentsOptions {
8826 anchor_segment_ids: Some(vec![line2_end_id]),
8827 drag_anchors: Vec::new(),
8828 commit_solved_initial_guesses: false,
8829 },
8830 )
8831 .await
8832 .unwrap();
8833
8834 assert!(
8835 !preview_delta.exec_outcome.var_solutions.is_empty(),
8836 "preview solve should still solve and return geometry feedback"
8837 );
8838 assert!(
8839 preview_source
8840 .text
8841 .contains("line1 = line(start = [var 1, var 2], end = [var 1, var 2])")
8842 );
8843 assert!(
8844 preview_source
8845 .text
8846 .contains("line2 = line(start = [var 5, var 6], end = [var 9, var 10])")
8847 );
8848
8849 let (mock_source, _) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
8850 assert_eq!(mock_source.text, preview_source.text);
8851
8852 mock_ctx.close().await;
8853 }
8854
8855 #[tokio::test(flavor = "multi_thread")]
8856 async fn test_add_constraint_persists_solver_feedback_for_next_mock_execute() {
8857 let initial_source = "\
8858sketch(on = XY) {
8859 line1 = line(start = [var 0, var 0], end = [var 10, var 0])
8860}
8861";
8862
8863 let program = Program::parse(initial_source).unwrap().0.unwrap();
8864 let mut frontend = FrontendState::new();
8865 let mock_ctx = ExecutorContext::new_mock(None).await;
8866 let version = Version(0);
8867
8868 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8869 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8870 let sketch_id = sketch_object.id;
8871 let sketch = expect_sketch(sketch_object);
8872 let line_end_id = *sketch.segments.get(1).unwrap();
8873
8874 let constraint = Constraint::Fixed(Fixed {
8875 points: vec![FixedPoint {
8876 point: line_end_id,
8877 position: Point2d {
8878 x: Number {
8879 value: 20.0,
8880 units: NumericSuffix::Mm,
8881 },
8882 y: Number {
8883 value: 0.0,
8884 units: NumericSuffix::Mm,
8885 },
8886 },
8887 }],
8888 });
8889 let (constraint_source, _) = frontend
8890 .add_constraint(&mock_ctx, version, sketch_id, constraint)
8891 .await
8892 .unwrap();
8893
8894 assert!(
8895 constraint_source
8896 .text
8897 .contains("line1 = line(start = [var 0, var 0], end = [var 20, var 0])"),
8898 "{}",
8899 constraint_source.text
8900 );
8901 let (mock_source, _) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
8902 assert_eq!(mock_source.text, constraint_source.text);
8903
8904 mock_ctx.close().await;
8905 }
8906
8907 #[test]
8908 fn test_no_solver_feedback_preserves_original_source() {
8909 let initial_source = "\
8910@settings(defaultLengthUnit = in, kclVersion = 2.0)
8911cylinder = startSketchOn(XY)
8912 |> circle(center= [0, 0], radius= 22)
8913 |> extrude(length = 14)
8914";
8915 let mut frontend = FrontendState::new();
8916 frontend.program = Program::parse(initial_source).unwrap().0.unwrap();
8917 let outcome = ExecOutcome {
8918 variables: Default::default(),
8919 operations: Default::default(),
8920 artifact_graph: Default::default(),
8921 scene_objects: Default::default(),
8922 source_range_to_object: Default::default(),
8923 var_solutions: Default::default(),
8924 issues: Default::default(),
8925 filenames: Default::default(),
8926 default_planes: Default::default(),
8927 };
8928
8929 let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
8930
8931 assert_eq!(source_delta.text, initial_source);
8932 }
8933
8934 #[tokio::test(flavor = "multi_thread")]
8937 async fn test_edit_segments_with_anchor_ids_limits_drag_fixed_constraints() {
8938 let initial_source = "\
8939sketch(on = XY) {
8940 point1 = point(at = [var 0mm, var 0mm])
8941 point2 = point(at = [var 0mm, var 0mm])
8942 coincident([point1, point2])
8943}
8944";
8945
8946 let program = Program::parse(initial_source).unwrap().0.unwrap();
8947 let mut frontend = FrontendState::new();
8948 let mock_ctx = ExecutorContext::new_mock(None).await;
8949 let version = Version(0);
8950
8951 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8952 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8953 let sketch_id = sketch_object.id;
8954 let sketch = expect_sketch(sketch_object);
8955 let point1_id = sketch.segments[0];
8956 let point2_id = sketch.segments[1];
8957
8958 let segments = vec![
8959 ExistingSegmentCtor {
8960 id: point1_id,
8961 ctor: SegmentCtor::Point(PointCtor {
8962 position: point_expr_mm(10.0, 0.0),
8963 }),
8964 },
8965 ExistingSegmentCtor {
8966 id: point2_id,
8967 ctor: SegmentCtor::Point(PointCtor {
8968 position: point_expr_mm(100.0, 0.0),
8969 }),
8970 },
8971 ];
8972 let (_, scene_delta) = frontend
8973 .edit_segments_with_options(
8974 &mock_ctx,
8975 version,
8976 sketch_id,
8977 segments,
8978 EditSegmentsOptions {
8979 anchor_segment_ids: Some(vec![point1_id]),
8980 drag_anchors: Vec::new(),
8981 commit_solved_initial_guesses: true,
8982 },
8983 )
8984 .await
8985 .unwrap();
8986
8987 assert_point_position_close(
8988 point_position(&scene_delta.new_graph, point1_id),
8989 point_number_mm(10.0, 0.0),
8990 );
8991 assert_point_position_close(
8992 point_position(&scene_delta.new_graph, point2_id),
8993 point_number_mm(10.0, 0.0),
8994 );
8995
8996 mock_ctx.close().await;
8997 }
8998
8999 fn collect_sketch_var_literals_with_value(program: &Program, value: f64) -> Vec<(SourceRange, ast::NodePath)> {
9002 use std::cell::RefCell;
9003 struct Collector {
9004 target: f64,
9005 out: RefCell<Vec<(SourceRange, ast::NodePath)>>,
9006 }
9007 impl<'a> crate::walk::Visitor<'a> for &Collector {
9008 type Error = crate::front::Error;
9009 fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
9010 if let crate::walk::Node::SketchVar(sketch_var) = node
9011 && let (Some(initial), Some(node_path)) = (&sketch_var.initial, &sketch_var.node_path)
9012 && (initial.value - self.target).abs() < 1e-9
9013 {
9014 self.out
9015 .borrow_mut()
9016 .push((SourceRange::from(initial.as_ref()), node_path.clone()));
9017 }
9018 for child in node.children().iter() {
9019 if !child.visit(*self)? {
9020 return Ok(false);
9021 }
9022 }
9023 Ok(true)
9024 }
9025 }
9026 let collector = Collector {
9027 target: value,
9028 out: Default::default(),
9029 };
9030 let _ = crate::walk::Node::from(&program.ast).visit(&collector);
9031 collector.out.into_inner()
9032 }
9033
9034 fn collect_all_sketch_vars(program: &Program) -> Vec<(SourceRange, ast::NodePath)> {
9037 use std::cell::RefCell;
9038 struct Collector {
9039 out: RefCell<Vec<(SourceRange, ast::NodePath)>>,
9040 }
9041 impl<'a> crate::walk::Visitor<'a> for &Collector {
9042 type Error = crate::front::Error;
9043 fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
9044 if let crate::walk::Node::SketchVar(sketch_var) = node
9045 && let Some(node_path) = &sketch_var.node_path
9046 {
9047 self.out
9048 .borrow_mut()
9049 .push((SourceRange::from(sketch_var), node_path.clone()));
9050 }
9051 for child in node.children().iter() {
9052 if !child.visit(*self)? {
9053 return Ok(false);
9054 }
9055 }
9056 Ok(true)
9057 }
9058 }
9059 let collector = Collector {
9060 out: Default::default(),
9061 };
9062 let _ = crate::walk::Node::from(&program.ast).visit(&collector);
9063 collector.out.into_inner()
9064 }
9065
9066 fn empty_exec_outcome_with_var_solutions(
9067 var_solutions: Vec<(SourceRange, Option<ast::NodePath>, Number)>,
9068 ) -> ExecOutcome {
9069 ExecOutcome {
9070 variables: Default::default(),
9071 operations: Default::default(),
9072 artifact_graph: Default::default(),
9073 scene_objects: Default::default(),
9074 source_range_to_object: Default::default(),
9075 var_solutions,
9076 issues: Default::default(),
9077 filenames: Default::default(),
9078 default_planes: Default::default(),
9079 }
9080 }
9081
9082 #[test]
9086 fn test_commit_var_solution_by_node_path_updates_sketch_var() {
9087 let initial_source = "\
9088sketch(on = XY) {
9089 line1 = line(start = [var 0, var 0], end = [var 10mm, var 0])
9090}
9091";
9092 let program = Program::parse(initial_source).unwrap().0.unwrap();
9093 let matches = collect_sketch_var_literals_with_value(&program, 10.0);
9094 assert_eq!(matches.len(), 1, "expected exactly one `var 10mm`");
9095 let (literal_range, node_path) = matches.into_iter().next().unwrap();
9096
9097 let mut frontend = FrontendState::new();
9098 frontend.program = program;
9099
9100 let outcome = empty_exec_outcome_with_var_solutions(vec![(
9101 literal_range,
9102 Some(node_path),
9103 Number {
9104 value: 25.0,
9105 units: NumericSuffix::Mm,
9106 },
9107 )]);
9108
9109 let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9110
9111 assert_eq!(
9112 source_delta.text,
9113 "\
9114sketch(on = XY) {
9115 line1 = line(start = [var 0, var 0], end = [var 25mm, var 0])
9116}
9117",
9118 );
9119 }
9120
9121 #[test]
9128 fn test_commit_var_solution_survives_whitespace_shift_earlier_in_file() {
9129 let compact_source = "\
9130sketch(on = XY) {
9131 line1 = line(start = [var 0, var 0], end = [var 10mm, var 0])
9132}
9133";
9134 let padded_source = "\
9135// added comment\n// added comment\n\nsketch(on = XY) {
9136 line1 = line(start = [var 0, var 0], end = [var 10mm, var 0])
9137}
9138";
9139 let compact_program = Program::parse(compact_source).unwrap().0.unwrap();
9140 let padded_program = Program::parse(padded_source).unwrap().0.unwrap();
9141
9142 let compact_match = collect_sketch_var_literals_with_value(&compact_program, 10.0)
9143 .into_iter()
9144 .next()
9145 .expect("expected `var 10mm` in compact source");
9146 let padded_match = collect_sketch_var_literals_with_value(&padded_program, 10.0)
9147 .into_iter()
9148 .next()
9149 .expect("expected `var 10mm` in padded source");
9150
9151 assert_ne!(
9152 compact_match.0, padded_match.0,
9153 "byte offsets must differ for this test to be meaningful"
9154 );
9155 assert_eq!(
9156 compact_match.1, padded_match.1,
9157 "node paths must agree across whitespace; that's the whole point of NodePath",
9158 );
9159
9160 let mut frontend = FrontendState::new();
9161 frontend.program = padded_program;
9162
9163 let outcome = empty_exec_outcome_with_var_solutions(vec![(
9165 compact_match.0,
9166 Some(compact_match.1),
9167 Number {
9168 value: 30.0,
9169 units: NumericSuffix::Mm,
9170 },
9171 )]);
9172
9173 let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9174
9175 assert_eq!(
9176 source_delta.text,
9177 "\
9178// added comment
9179// added comment
9180
9181sketch(on = XY) {
9182 line1 = line(start = [var 0, var 0], end = [var 30mm, var 0])
9183}
9184",
9185 );
9186 }
9187
9188 #[test]
9192 fn test_commit_var_solution_node_path_wins_when_source_range_points_at_wrong_var() {
9193 let initial_source = "\
9194sketch(on = XY) {
9195 line1 = line(start = [var 10mm, var 0mm], end = [var 20mm, var 0mm])
9196}
9197";
9198 let program = Program::parse(initial_source).unwrap().0.unwrap();
9199
9200 let var_10 = collect_sketch_var_literals_with_value(&program, 10.0)
9201 .into_iter()
9202 .next()
9203 .expect("expected `var 10mm`");
9204 let var_20 = collect_sketch_var_literals_with_value(&program, 20.0)
9205 .into_iter()
9206 .next()
9207 .expect("expected `var 20mm`");
9208
9209 let mut frontend = FrontendState::new();
9210 frontend.program = program;
9211
9212 let outcome = empty_exec_outcome_with_var_solutions(vec![(
9214 var_20.0,
9215 Some(var_10.1),
9216 Number {
9217 value: 33.0,
9218 units: NumericSuffix::Mm,
9219 },
9220 )]);
9221
9222 let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9223
9224 assert_eq!(
9225 source_delta.text,
9226 "\
9227sketch(on = XY) {
9228 line1 = line(start = [var 33mm, var 0mm], end = [var 20mm, var 0mm])
9229}
9230",
9231 );
9232 }
9233
9234 #[test]
9241 fn test_commit_var_solution_writes_back_into_bare_var() {
9242 let initial_source = "\
9243@settings(experimentalFeatures = allow, kclVersion = 2.0)
9244sketch(on = XY) {
9245 line1 = line(start = [var, var 0mm], end = [var 10mm, var 0])
9246}
9247";
9248 let program = Program::parse(initial_source).unwrap().0.unwrap();
9249
9250 let bare = collect_all_sketch_vars(&program)
9253 .into_iter()
9254 .find(|(range, _)| {
9255 range.end() - range.start() == 3
9257 })
9258 .expect("expected at least one bare `var`");
9259
9260 let mut frontend = FrontendState::new();
9261 frontend.program = program;
9262
9263 let outcome = empty_exec_outcome_with_var_solutions(vec![(
9264 bare.0,
9265 Some(bare.1),
9266 Number {
9267 value: 7.0,
9268 units: NumericSuffix::Mm,
9269 },
9270 )]);
9271
9272 let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9273
9274 assert_eq!(
9278 source_delta.text,
9279 "\
9280@settings(experimentalFeatures = allow, kclVersion = 2.0)
9281
9282sketch(on = XY) {
9283 line1 = line(start = [var 7mm, var 0mm], end = [var 10mm, var 0])
9284}
9285",
9286 );
9287 }
9288
9289 #[tokio::test(flavor = "multi_thread")]
9290 async fn test_delete_point_without_var() {
9291 let initial_source = "\
9292sketch(on = XY) {
9293 point(at = [var 1, var 2])
9294 point(at = [var 3, var 4])
9295 point(at = [var 5, var 6])
9296}
9297";
9298
9299 let program = Program::parse(initial_source).unwrap().0.unwrap();
9300
9301 let mut frontend = FrontendState::new();
9302
9303 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9304 let mock_ctx = ExecutorContext::new_mock(None).await;
9305 let version = Version(0);
9306
9307 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9308 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9309 let sketch_id = sketch_object.id;
9310 let sketch = expect_sketch(sketch_object);
9311
9312 let point_id = *sketch.segments.get(1).unwrap();
9313
9314 let (src_delta, scene_delta) = frontend
9315 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
9316 .await
9317 .unwrap();
9318 assert_eq!(
9319 src_delta.text.as_str(),
9320 "\
9321sketch(on = XY) {
9322 point(at = [var 1, var 2])
9323 point(at = [var 5, var 6])
9324}
9325"
9326 );
9327 assert_eq!(scene_delta.new_objects, vec![]);
9328 assert_eq!(scene_delta.new_graph.objects.len(), 4);
9329
9330 ctx.close().await;
9331 mock_ctx.close().await;
9332 }
9333
9334 #[tokio::test(flavor = "multi_thread")]
9335 async fn test_delete_point_with_var() {
9336 let initial_source = "\
9337sketch(on = XY) {
9338 point(at = [var 1, var 2])
9339 point1 = point(at = [var 3, var 4])
9340 point(at = [var 5, var 6])
9341}
9342";
9343
9344 let program = Program::parse(initial_source).unwrap().0.unwrap();
9345
9346 let mut frontend = FrontendState::new();
9347
9348 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9349 let mock_ctx = ExecutorContext::new_mock(None).await;
9350 let version = Version(0);
9351
9352 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9353 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9354 let sketch_id = sketch_object.id;
9355 let sketch = expect_sketch(sketch_object);
9356
9357 let point_id = *sketch.segments.get(1).unwrap();
9358
9359 let (src_delta, scene_delta) = frontend
9360 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
9361 .await
9362 .unwrap();
9363 assert_eq!(
9364 src_delta.text.as_str(),
9365 "\
9366sketch(on = XY) {
9367 point(at = [var 1, var 2])
9368 point(at = [var 5, var 6])
9369}
9370"
9371 );
9372 assert_eq!(scene_delta.new_objects, vec![]);
9373 assert_eq!(scene_delta.new_graph.objects.len(), 4);
9374
9375 ctx.close().await;
9376 mock_ctx.close().await;
9377 }
9378
9379 #[tokio::test(flavor = "multi_thread")]
9380 async fn test_delete_multiple_points() {
9381 let initial_source = "\
9382sketch(on = XY) {
9383 point(at = [var 1, var 2])
9384 point1 = point(at = [var 3, var 4])
9385 point(at = [var 5, var 6])
9386}
9387";
9388
9389 let program = Program::parse(initial_source).unwrap().0.unwrap();
9390
9391 let mut frontend = FrontendState::new();
9392
9393 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9394 let mock_ctx = ExecutorContext::new_mock(None).await;
9395 let version = Version(0);
9396
9397 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9398 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9399 let sketch_id = sketch_object.id;
9400
9401 let sketch = expect_sketch(sketch_object);
9402
9403 let point1_id = *sketch.segments.first().unwrap();
9404 let point2_id = *sketch.segments.get(1).unwrap();
9405
9406 let (src_delta, scene_delta) = frontend
9407 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point1_id, point2_id])
9408 .await
9409 .unwrap();
9410 assert_eq!(
9411 src_delta.text.as_str(),
9412 "\
9413sketch(on = XY) {
9414 point(at = [var 5, var 6])
9415}
9416"
9417 );
9418 assert_eq!(scene_delta.new_objects, vec![]);
9419 assert_eq!(scene_delta.new_graph.objects.len(), 3);
9420
9421 ctx.close().await;
9422 mock_ctx.close().await;
9423 }
9424
9425 #[tokio::test(flavor = "multi_thread")]
9426 async fn test_delete_coincident_constraint() {
9427 let initial_source = "\
9428sketch(on = XY) {
9429 point1 = point(at = [var 1, var 2])
9430 point2 = point(at = [var 3, var 4])
9431 coincident([point1, point2])
9432 point(at = [var 5, var 6])
9433}
9434";
9435
9436 let program = Program::parse(initial_source).unwrap().0.unwrap();
9437
9438 let mut frontend = FrontendState::new();
9439
9440 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9441 let mock_ctx = ExecutorContext::new_mock(None).await;
9442 let version = Version(0);
9443
9444 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9445 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9446 let sketch_id = sketch_object.id;
9447 let sketch = expect_sketch(sketch_object);
9448
9449 let coincident_id = *sketch.constraints.first().unwrap();
9450
9451 let (src_delta, scene_delta) = frontend
9452 .delete_objects(&mock_ctx, version, sketch_id, vec![coincident_id], Vec::new())
9453 .await
9454 .unwrap();
9455 assert_eq!(
9456 src_delta.text.as_str(),
9457 "\
9458sketch(on = XY) {
9459 point1 = point(at = [var 1, var 2])
9460 point2 = point(at = [var 3, var 4])
9461 point(at = [var 5, var 6])
9462}
9463"
9464 );
9465 assert_eq!(scene_delta.new_objects, vec![]);
9466 assert_eq!(scene_delta.new_graph.objects.len(), 5);
9467
9468 ctx.close().await;
9469 mock_ctx.close().await;
9470 }
9471
9472 #[tokio::test(flavor = "multi_thread")]
9473 async fn test_delete_line_cascades_to_coincident_constraint() {
9474 let initial_source = "\
9475sketch(on = XY) {
9476 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9477 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9478 coincident([line1.end, line2.start])
9479}
9480";
9481
9482 let program = Program::parse(initial_source).unwrap().0.unwrap();
9483
9484 let mut frontend = FrontendState::new();
9485
9486 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9487 let mock_ctx = ExecutorContext::new_mock(None).await;
9488 let version = Version(0);
9489
9490 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9491 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9492 let sketch_id = sketch_object.id;
9493 let sketch = expect_sketch(sketch_object);
9494 let line_id = *sketch.segments.get(5).unwrap();
9495
9496 let (src_delta, scene_delta) = frontend
9497 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
9498 .await
9499 .unwrap();
9500 assert_eq!(
9501 src_delta.text.as_str(),
9502 "\
9503sketch(on = XY) {
9504 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9505}
9506"
9507 );
9508 assert_eq!(
9509 scene_delta.new_graph.objects.len(),
9510 5,
9511 "{:#?}",
9512 scene_delta.new_graph.objects
9513 );
9514
9515 ctx.close().await;
9516 mock_ctx.close().await;
9517 }
9518
9519 #[tokio::test(flavor = "multi_thread")]
9520 async fn test_delete_line_cascades_to_distance_constraint() {
9521 let initial_source = "\
9522sketch(on = XY) {
9523 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9524 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9525 distance([line1.end, line2.start]) == 10mm
9526}
9527";
9528
9529 let program = Program::parse(initial_source).unwrap().0.unwrap();
9530
9531 let mut frontend = FrontendState::new();
9532
9533 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9534 let mock_ctx = ExecutorContext::new_mock(None).await;
9535 let version = Version(0);
9536
9537 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
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 line_id = *sketch.segments.get(5).unwrap();
9542
9543 let (src_delta, scene_delta) = frontend
9544 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
9545 .await
9546 .unwrap();
9547 assert_eq!(
9548 src_delta.text.as_str(),
9549 "\
9550sketch(on = XY) {
9551 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9552}
9553"
9554 );
9555 assert_eq!(
9556 scene_delta.new_graph.objects.len(),
9557 5,
9558 "{:#?}",
9559 scene_delta.new_graph.objects
9560 );
9561
9562 ctx.close().await;
9563 mock_ctx.close().await;
9564 }
9565
9566 #[tokio::test(flavor = "multi_thread")]
9567 async fn test_delete_point_cascades_to_horizontal_distance_constraint() {
9568 let initial_source = "\
9569sketch(on = XY) {
9570 point1 = point(at = [var 1, var 2])
9571 point2 = point(at = [var 3, var 4])
9572 horizontalDistance([point1, point2]) == 10mm
9573}
9574";
9575
9576 let program = Program::parse(initial_source).unwrap().0.unwrap();
9577
9578 let mut frontend = FrontendState::new();
9579
9580 let mock_ctx = ExecutorContext::new_mock(None).await;
9581 let version = Version(0);
9582
9583 frontend.program = program.clone();
9584 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9585 frontend.update_state_after_exec(outcome, true);
9586 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9587 let sketch_id = sketch_object.id;
9588 let sketch = expect_sketch(sketch_object);
9589 let point2_id = *sketch.segments.get(1).unwrap();
9590
9591 let (src_delta, scene_delta) = frontend
9592 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point2_id])
9593 .await
9594 .unwrap();
9595 assert_eq!(
9596 src_delta.text.as_str(),
9597 "\
9598sketch(on = XY) {
9599 point1 = point(at = [var 1, var 2])
9600}
9601"
9602 );
9603 assert_eq!(
9604 scene_delta.new_graph.objects.len(),
9605 3,
9606 "{:#?}",
9607 scene_delta.new_graph.objects
9608 );
9609
9610 mock_ctx.close().await;
9611 }
9612
9613 #[tokio::test(flavor = "multi_thread")]
9614 async fn test_delete_line_cascades_to_fixed_constraint() {
9615 let initial_source = "\
9616sketch(on = XY) {
9617 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9618 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9619 fixed([line1.start, [0, 0]])
9620}
9621";
9622
9623 let program = Program::parse(initial_source).unwrap().0.unwrap();
9624
9625 let mut frontend = FrontendState::new();
9626
9627 let mock_ctx = ExecutorContext::new_mock(None).await;
9628 let version = Version(0);
9629
9630 frontend.program = program.clone();
9631 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9632 frontend.update_state_after_exec(outcome, true);
9633 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9634 let sketch_id = sketch_object.id;
9635 let sketch = expect_sketch(sketch_object);
9636 let line1_id = *sketch.segments.get(2).unwrap();
9637
9638 let (src_delta, scene_delta) = frontend
9639 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
9640 .await
9641 .unwrap();
9642 assert_eq!(
9643 src_delta.text.as_str(),
9644 "\
9645sketch(on = XY) {
9646 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9647}
9648"
9649 );
9650 assert_eq!(
9651 scene_delta.new_graph.objects.len(),
9652 5,
9653 "{:#?}",
9654 scene_delta.new_graph.objects
9655 );
9656
9657 mock_ctx.close().await;
9658 }
9659
9660 #[tokio::test(flavor = "multi_thread")]
9661 async fn test_delete_line_cascades_to_midpoint_constraint() {
9662 let initial_source = "\
9663sketch(on = XY) {
9664 point1 = point(at = [var 1, var 2])
9665 line1 = line(start = [var 0, var 0], end = [var 6, var 4])
9666 midpoint(line1, point = point1)
9667}
9668";
9669
9670 let program = Program::parse(initial_source).unwrap().0.unwrap();
9671
9672 let mut frontend = FrontendState::new();
9673
9674 let mock_ctx = ExecutorContext::new_mock(None).await;
9675 let version = Version(0);
9676
9677 frontend.program = program.clone();
9678 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9679 frontend.update_state_after_exec(outcome, true);
9680 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9681 let sketch_id = sketch_object.id;
9682 let sketch = expect_sketch(sketch_object);
9683 let line1_id = *sketch.segments.get(3).unwrap();
9684
9685 let (src_delta, scene_delta) = frontend
9686 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
9687 .await
9688 .unwrap();
9689 assert_eq!(
9690 src_delta.text.as_str(),
9691 "\
9692sketch(on = XY) {
9693 point1 = point(at = [var 1, var 2])
9694}
9695"
9696 );
9697 assert_eq!(
9698 scene_delta.new_graph.objects.len(),
9699 3,
9700 "{:#?}",
9701 scene_delta.new_graph.objects
9702 );
9703
9704 mock_ctx.close().await;
9705 }
9706
9707 #[tokio::test(flavor = "multi_thread")]
9708 async fn test_delete_point_preserves_multiline_coincident_constraint() {
9709 let initial_source = "\
9710sketch(on = XY) {
9711 point1 = point(at = [var 1, var 2])
9712 point2 = point(at = [var 3, var 4])
9713 point3 = point(at = [var 5, var 6])
9714 coincident([point1, point2, point3])
9715}
9716";
9717
9718 let program = Program::parse(initial_source).unwrap().0.unwrap();
9719
9720 let mut frontend = FrontendState::new();
9721
9722 let mock_ctx = ExecutorContext::new_mock(None).await;
9723 let version = Version(0);
9724
9725 frontend.program = program.clone();
9726 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9727 frontend.update_state_after_exec(outcome, true);
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 point3_id = *sketch.segments.get(2).unwrap();
9732
9733 let (src_delta, scene_delta) = frontend
9734 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point3_id])
9735 .await
9736 .unwrap();
9737 assert!(src_delta.text.contains("point1 = point("), "{}", src_delta.text);
9738 assert!(src_delta.text.contains("point2 = point("), "{}", src_delta.text);
9739 assert!(!src_delta.text.contains("point3 = point("), "{}", src_delta.text);
9740 assert!(
9741 src_delta.text.contains("coincident([point1, point2])"),
9742 "{}",
9743 src_delta.text
9744 );
9745
9746 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9747 let sketch = expect_sketch(sketch_object);
9748 assert_eq!(sketch.segments.len(), 2);
9749 assert_eq!(sketch.constraints.len(), 1);
9750
9751 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
9752 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9753 panic!("Expected constraint object");
9754 };
9755 let Constraint::Coincident(coincident) = constraint else {
9756 panic!("Expected coincident constraint");
9757 };
9758 assert_eq!(
9759 coincident.segments,
9760 sketch
9761 .segments
9762 .iter()
9763 .copied()
9764 .map(Into::into)
9765 .collect::<Vec<ConstraintSegment>>()
9766 );
9767
9768 mock_ctx.close().await;
9769 }
9770
9771 #[tokio::test(flavor = "multi_thread")]
9772 async fn test_delete_line_preserves_multiline_equal_length_constraint() {
9773 let initial_source = "\
9774sketch(on = XY) {
9775 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9776 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9777 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
9778 equalLength([line1, line2, line3])
9779}
9780";
9781
9782 let program = Program::parse(initial_source).unwrap().0.unwrap();
9783
9784 let mut frontend = FrontendState::new();
9785
9786 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9787 let mock_ctx = ExecutorContext::new_mock(None).await;
9788 let version = Version(0);
9789
9790 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9791 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9792 let sketch_id = sketch_object.id;
9793 let sketch = expect_sketch(sketch_object);
9794 let line3_id = *sketch.segments.get(8).unwrap();
9795
9796 let (src_delta, scene_delta) = frontend
9797 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line3_id])
9798 .await
9799 .unwrap();
9800 assert_eq!(
9801 src_delta.text.as_str(),
9802 "\
9803sketch(on = XY) {
9804 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9805 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9806 equalLength([line1, line2])
9807}
9808"
9809 );
9810
9811 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9812 let sketch = expect_sketch(sketch_object);
9813 assert_eq!(sketch.constraints.len(), 1);
9814
9815 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
9816 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9817 panic!("Expected constraint object");
9818 };
9819 let Constraint::LinesEqualLength(lines_equal_length) = constraint else {
9820 panic!("Expected lines equal length constraint");
9821 };
9822 assert_eq!(lines_equal_length.lines.len(), 2);
9823
9824 ctx.close().await;
9825 mock_ctx.close().await;
9826 }
9827
9828 #[tokio::test(flavor = "multi_thread")]
9829 async fn test_delete_line_preserves_multiline_horizontal_constraint() {
9830 let initial_source = "\
9831sketch(on = XY) {
9832 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9833 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9834 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
9835 horizontal([line1.end, line2.start, line3.start])
9836}
9837";
9838
9839 let program = Program::parse(initial_source).unwrap().0.unwrap();
9840
9841 let mut frontend = FrontendState::new();
9842
9843 let mock_ctx = ExecutorContext::new_mock(None).await;
9844 let version = Version(0);
9845
9846 frontend.program = program.clone();
9847 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9848 frontend.update_state_after_exec(outcome, true);
9849 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9850 let sketch_id = sketch_object.id;
9851 let sketch = expect_sketch(sketch_object);
9852 let line1_id = *sketch.segments.get(2).unwrap();
9853
9854 let (src_delta, scene_delta) = frontend
9855 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
9856 .await
9857 .unwrap();
9858 assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
9859 assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
9860 assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
9861 assert!(
9862 src_delta.text.contains("horizontal([line2.start, line3.start])"),
9863 "{}",
9864 src_delta.text
9865 );
9866
9867 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9868 let sketch = expect_sketch(sketch_object);
9869 assert_eq!(sketch.constraints.len(), 1);
9870
9871 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
9872 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9873 panic!("Expected constraint object");
9874 };
9875 let Constraint::Horizontal(Horizontal::Points { points }) = constraint else {
9876 panic!("Expected horizontal points constraint");
9877 };
9878 let remaining_points = vec![sketch.segments[0].into(), sketch.segments[3].into()];
9879 assert_eq!(*points, remaining_points);
9880
9881 mock_ctx.close().await;
9882 }
9883
9884 #[tokio::test(flavor = "multi_thread")]
9885 async fn test_delete_line_preserves_multiline_vertical_constraint() {
9886 let initial_source = "\
9887sketch(on = XY) {
9888 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9889 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9890 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
9891 vertical([line1.end, line2.start, line3.start])
9892}
9893";
9894
9895 let program = Program::parse(initial_source).unwrap().0.unwrap();
9896
9897 let mut frontend = FrontendState::new();
9898
9899 let mock_ctx = ExecutorContext::new_mock(None).await;
9900 let version = Version(0);
9901
9902 frontend.program = program.clone();
9903 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9904 frontend.update_state_after_exec(outcome, true);
9905 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9906 let sketch_id = sketch_object.id;
9907 let sketch = expect_sketch(sketch_object);
9908 let line1_id = *sketch.segments.get(2).unwrap();
9909
9910 let (src_delta, scene_delta) = frontend
9911 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
9912 .await
9913 .unwrap();
9914 assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
9915 assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
9916 assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
9917 assert!(
9918 src_delta.text.contains("vertical([line2.start, line3.start])"),
9919 "{}",
9920 src_delta.text
9921 );
9922
9923 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9924 let sketch = expect_sketch(sketch_object);
9925 assert_eq!(sketch.constraints.len(), 1);
9926
9927 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
9928 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9929 panic!("Expected constraint object");
9930 };
9931 let Constraint::Vertical(Vertical::Points { points }) = constraint else {
9932 panic!("Expected vertical points constraint");
9933 };
9934 let remaining_points = vec![sketch.segments[0].into(), sketch.segments[3].into()];
9935 assert_eq!(*points, remaining_points);
9936
9937 mock_ctx.close().await;
9938 }
9939
9940 #[tokio::test(flavor = "multi_thread")]
9941 async fn test_delete_line_preserves_multiline_coincident_constraint() {
9942 let initial_source = "\
9943sketch(on = XY) {
9944 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9945 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9946 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
9947 coincident([line1.end, line2.start, line3.start])
9948}
9949";
9950
9951 let program = Program::parse(initial_source).unwrap().0.unwrap();
9952
9953 let mut frontend = FrontendState::new();
9954
9955 let mock_ctx = ExecutorContext::new_mock(None).await;
9956 let version = Version(0);
9957
9958 frontend.program = program.clone();
9959 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9960 frontend.update_state_after_exec(outcome, true);
9961 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9962 let sketch_id = sketch_object.id;
9963 let sketch = expect_sketch(sketch_object);
9964 let line1_id = *sketch.segments.get(2).unwrap();
9965
9966 let (src_delta, scene_delta) = frontend
9967 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
9968 .await
9969 .unwrap();
9970 assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
9971 assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
9972 assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
9973 assert!(
9974 src_delta.text.contains("coincident([line2.start, line3.start])"),
9975 "{}",
9976 src_delta.text
9977 );
9978
9979 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9980 let sketch = expect_sketch(sketch_object);
9981 assert_eq!(sketch.constraints.len(), 1);
9982
9983 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
9984 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9985 panic!("Expected constraint object");
9986 };
9987 let Constraint::Coincident(coincident) = constraint else {
9988 panic!("Expected coincident constraint");
9989 };
9990 let remaining_segments = vec![sketch.segments[0].into(), sketch.segments[3].into()];
9991 assert_eq!(coincident.segments, remaining_segments);
9992
9993 mock_ctx.close().await;
9994 }
9995
9996 #[tokio::test(flavor = "multi_thread")]
9997 async fn test_delete_lines_removes_multiline_equal_length_constraint_below_minimum() {
9998 let initial_source = "\
9999sketch(on = XY) {
10000 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10001 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10002 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10003 equalLength([line1, line2, line3])
10004}
10005";
10006
10007 let program = Program::parse(initial_source).unwrap().0.unwrap();
10008
10009 let mut frontend = FrontendState::new();
10010
10011 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10012 let mock_ctx = ExecutorContext::new_mock(None).await;
10013 let version = Version(0);
10014
10015 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10016 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10017 let sketch_id = sketch_object.id;
10018 let sketch = expect_sketch(sketch_object);
10019 let line2_id = *sketch.segments.get(5).unwrap();
10020 let line3_id = *sketch.segments.get(8).unwrap();
10021
10022 let (src_delta, scene_delta) = frontend
10023 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line2_id, line3_id])
10024 .await
10025 .unwrap();
10026 assert_eq!(
10027 src_delta.text.as_str(),
10028 "\
10029sketch(on = XY) {
10030 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10031}
10032"
10033 );
10034
10035 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10036 let sketch = expect_sketch(sketch_object);
10037 assert!(sketch.constraints.is_empty());
10038
10039 ctx.close().await;
10040 mock_ctx.close().await;
10041 }
10042
10043 #[tokio::test(flavor = "multi_thread")]
10044 async fn test_delete_line_preserves_multiline_parallel_constraint() {
10045 let initial_source = "\
10046sketch(on = XY) {
10047 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10048 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10049 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10050 parallel([line1, line2, line3])
10051}
10052";
10053
10054 let program = Program::parse(initial_source).unwrap().0.unwrap();
10055
10056 let mut frontend = FrontendState::new();
10057
10058 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10059 let mock_ctx = ExecutorContext::new_mock(None).await;
10060 let version = Version(0);
10061
10062 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10063 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10064 let sketch_id = sketch_object.id;
10065 let sketch = expect_sketch(sketch_object);
10066 let line3_id = *sketch.segments.get(8).unwrap();
10067
10068 let (src_delta, scene_delta) = frontend
10069 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line3_id])
10070 .await
10071 .unwrap();
10072 assert_eq!(
10073 src_delta.text.as_str(),
10074 "\
10075sketch(on = XY) {
10076 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10077 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10078 parallel([line1, line2])
10079}
10080"
10081 );
10082
10083 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10084 let sketch = expect_sketch(sketch_object);
10085 assert_eq!(sketch.constraints.len(), 1);
10086
10087 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10088 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10089 panic!("Expected constraint object");
10090 };
10091 let Constraint::Parallel(parallel) = constraint else {
10092 panic!("Expected parallel constraint");
10093 };
10094 assert_eq!(parallel.lines.len(), 2);
10095
10096 ctx.close().await;
10097 mock_ctx.close().await;
10098 }
10099
10100 #[tokio::test(flavor = "multi_thread")]
10101 async fn test_delete_lines_removes_multiline_parallel_constraint_below_minimum() {
10102 let initial_source = "\
10103sketch(on = XY) {
10104 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10105 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10106 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10107 parallel([line1, line2, line3])
10108}
10109";
10110
10111 let program = Program::parse(initial_source).unwrap().0.unwrap();
10112
10113 let mut frontend = FrontendState::new();
10114
10115 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10116 let mock_ctx = ExecutorContext::new_mock(None).await;
10117 let version = Version(0);
10118
10119 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10120 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10121 let sketch_id = sketch_object.id;
10122 let sketch = expect_sketch(sketch_object);
10123 let line2_id = *sketch.segments.get(5).unwrap();
10124 let line3_id = *sketch.segments.get(8).unwrap();
10125
10126 let (src_delta, scene_delta) = frontend
10127 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line2_id, line3_id])
10128 .await
10129 .unwrap();
10130 assert_eq!(
10131 src_delta.text.as_str(),
10132 "\
10133sketch(on = XY) {
10134 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10135}
10136"
10137 );
10138
10139 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10140 let sketch = expect_sketch(sketch_object);
10141 assert!(sketch.constraints.is_empty());
10142
10143 ctx.close().await;
10144 mock_ctx.close().await;
10145 }
10146
10147 #[tokio::test(flavor = "multi_thread")]
10148 async fn test_delete_line_line_coincident_constraint() {
10149 let initial_source = "\
10150sketch(on = XY) {
10151 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10152 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10153 coincident([line1, line2])
10154}
10155";
10156
10157 let program = Program::parse(initial_source).unwrap().0.unwrap();
10158
10159 let mut frontend = FrontendState::new();
10160
10161 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10162 let mock_ctx = ExecutorContext::new_mock(None).await;
10163 let version = Version(0);
10164
10165 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10166 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10167 let sketch_id = sketch_object.id;
10168 let sketch = expect_sketch(sketch_object);
10169
10170 let coincident_id = *sketch.constraints.first().unwrap();
10171
10172 let (src_delta, scene_delta) = frontend
10173 .delete_objects(&mock_ctx, version, sketch_id, vec![coincident_id], Vec::new())
10174 .await
10175 .unwrap();
10176 assert_eq!(
10177 src_delta.text.as_str(),
10178 "\
10179sketch(on = XY) {
10180 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10181 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10182}
10183"
10184 );
10185 assert_eq!(scene_delta.new_objects, vec![]);
10186 assert_eq!(scene_delta.new_graph.objects.len(), 8);
10187
10188 ctx.close().await;
10189 mock_ctx.close().await;
10190 }
10191
10192 #[tokio::test(flavor = "multi_thread")]
10193 async fn test_two_points_coincident() {
10194 let initial_source = "\
10195sketch(on = XY) {
10196 point1 = point(at = [var 1, var 2])
10197 point(at = [3, 4])
10198}
10199";
10200
10201 let program = Program::parse(initial_source).unwrap().0.unwrap();
10202
10203 let mut frontend = FrontendState::new();
10204
10205 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10206 let mock_ctx = ExecutorContext::new_mock(None).await;
10207 let version = Version(0);
10208
10209 frontend.hack_set_program(&ctx, program).await.unwrap();
10210 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10211 let sketch_id = sketch_object.id;
10212 let sketch = expect_sketch(sketch_object);
10213 let point0_id = *sketch.segments.first().unwrap();
10214 let point1_id = *sketch.segments.get(1).unwrap();
10215
10216 let constraint = Constraint::Coincident(Coincident {
10217 segments: vec![point0_id.into(), point1_id.into()],
10218 });
10219 let (src_delta, scene_delta) = frontend
10220 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10221 .await
10222 .unwrap();
10223 assert_eq!(
10224 src_delta.text.as_str(),
10225 "\
10226sketch(on = XY) {
10227 point1 = point(at = [var 3, var 4])
10228 point2 = point(at = [3, 4])
10229 coincident([point1, point2])
10230}
10231"
10232 );
10233 assert_eq!(
10234 scene_delta.new_graph.objects.len(),
10235 5,
10236 "{:#?}",
10237 scene_delta.new_graph.objects
10238 );
10239
10240 ctx.close().await;
10241 mock_ctx.close().await;
10242 }
10243
10244 #[tokio::test(flavor = "multi_thread")]
10245 async fn test_three_points_coincident() {
10246 let initial_source = "\
10247sketch(on = XY) {
10248 point1 = point(at = [var 1, var 2])
10249 point(at = [var 3, var 4])
10250 point(at = [var 5, var 6])
10251}
10252";
10253
10254 let program = Program::parse(initial_source).unwrap().0.unwrap();
10255
10256 let mut frontend = FrontendState::new();
10257
10258 let mock_ctx = ExecutorContext::new_mock(None).await;
10259 let version = Version(0);
10260
10261 frontend.program = program.clone();
10262 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10263 frontend.update_state_after_exec(outcome, true);
10264 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10265 let sketch_id = sketch_object.id;
10266 let sketch = expect_sketch(sketch_object);
10267 let segments = sketch
10268 .segments
10269 .iter()
10270 .take(3)
10271 .copied()
10272 .map(Into::into)
10273 .collect::<Vec<ConstraintSegment>>();
10274
10275 let constraint = Constraint::Coincident(Coincident {
10276 segments: segments.clone(),
10277 });
10278 let (src_delta, scene_delta) = frontend
10279 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10280 .await
10281 .unwrap();
10282 assert_eq!(
10283 src_delta.text.as_str(),
10284 "\
10285sketch(on = XY) {
10286 point1 = point(at = [var 3, var 4])
10287 point2 = point(at = [var 3, var 4])
10288 point3 = point(at = [var 3, var 4])
10289 coincident([point1, point2, point3])
10290}
10291"
10292 );
10293
10294 let constraint_object = scene_delta
10295 .new_graph
10296 .objects
10297 .iter()
10298 .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
10299 .unwrap();
10300
10301 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10302 panic!("expected a constraint object");
10303 };
10304
10305 assert_eq!(constraint, &Constraint::Coincident(Coincident { segments }));
10306
10307 mock_ctx.close().await;
10308 }
10309
10310 #[tokio::test(flavor = "multi_thread")]
10311 async fn test_source_with_three_point_coincident_tracks_all_segments() {
10312 let initial_source = "\
10313sketch(on = XY) {
10314 point1 = point(at = [var 1, var 2])
10315 point2 = point(at = [var 3, var 4])
10316 point3 = point(at = [var 5, var 6])
10317 coincident([point1, point2, point3])
10318}
10319";
10320
10321 let program = Program::parse(initial_source).unwrap().0.unwrap();
10322
10323 let mut frontend = FrontendState::new();
10324
10325 let ctx = ExecutorContext::new_mock(None).await;
10326 frontend.program = program.clone();
10327 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10328 frontend.update_state_after_exec(outcome, true);
10329
10330 let constraint_object = frontend
10331 .scene_graph
10332 .objects
10333 .iter()
10334 .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
10335 .unwrap();
10336 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10337 panic!("expected a constraint object");
10338 };
10339
10340 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10341 let sketch = expect_sketch(sketch_object);
10342 let expected_segments = sketch
10343 .segments
10344 .iter()
10345 .take(3)
10346 .copied()
10347 .map(Into::into)
10348 .collect::<Vec<ConstraintSegment>>();
10349
10350 assert_eq!(
10351 constraint,
10352 &Constraint::Coincident(Coincident {
10353 segments: expected_segments,
10354 })
10355 );
10356
10357 ctx.close().await;
10358 }
10359
10360 #[tokio::test(flavor = "multi_thread")]
10361 async fn test_point_origin_coincident_preserves_order() {
10362 let initial_source = "\
10363sketch(on = XY) {
10364 point(at = [var 1, var 2])
10365}
10366";
10367
10368 for (origin_first, expected_source) in [
10369 (
10370 true,
10371 "\
10372sketch(on = XY) {
10373 point1 = point(at = [var 0, var 0])
10374 coincident([ORIGIN, point1])
10375}
10376",
10377 ),
10378 (
10379 false,
10380 "\
10381sketch(on = XY) {
10382 point1 = point(at = [var 0, var 0])
10383 coincident([point1, ORIGIN])
10384}
10385",
10386 ),
10387 ] {
10388 let program = Program::parse(initial_source).unwrap().0.unwrap();
10389
10390 let mut frontend = FrontendState::new();
10391
10392 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10393 let mock_ctx = ExecutorContext::new_mock(None).await;
10394 let version = Version(0);
10395
10396 frontend.hack_set_program(&ctx, program).await.unwrap();
10397 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10398 let sketch_id = sketch_object.id;
10399 let sketch = expect_sketch(sketch_object);
10400 let point_id = *sketch.segments.first().unwrap();
10401
10402 let segments = if origin_first {
10403 vec![ConstraintSegment::ORIGIN, point_id.into()]
10404 } else {
10405 vec![point_id.into(), ConstraintSegment::ORIGIN]
10406 };
10407 let constraint = Constraint::Coincident(Coincident {
10408 segments: segments.clone(),
10409 });
10410 let (src_delta, scene_delta) = frontend
10411 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10412 .await
10413 .unwrap();
10414 assert_eq!(src_delta.text.as_str(), expected_source);
10415
10416 let constraint_object = scene_delta
10417 .new_graph
10418 .objects
10419 .iter()
10420 .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
10421 .unwrap();
10422
10423 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10424 panic!("expected a constraint object");
10425 };
10426
10427 assert_eq!(constraint, &Constraint::Coincident(Coincident { segments }));
10428
10429 ctx.close().await;
10430 mock_ctx.close().await;
10431 }
10432 }
10433
10434 #[tokio::test(flavor = "multi_thread")]
10435 async fn test_coincident_of_line_end_points() {
10436 let initial_source = "\
10437sketch(on = XY) {
10438 line(start = [var 1, var 2], end = [var 3, var 4])
10439 line(start = [var 5, var 6], end = [var 7, var 8])
10440}
10441";
10442
10443 let program = Program::parse(initial_source).unwrap().0.unwrap();
10444
10445 let mut frontend = FrontendState::new();
10446
10447 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10448 let mock_ctx = ExecutorContext::new_mock(None).await;
10449 let version = Version(0);
10450
10451 frontend.hack_set_program(&ctx, program).await.unwrap();
10452 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10453 let sketch_id = sketch_object.id;
10454 let sketch = expect_sketch(sketch_object);
10455 let point0_id = *sketch.segments.get(1).unwrap();
10456 let point1_id = *sketch.segments.get(3).unwrap();
10457
10458 let constraint = Constraint::Coincident(Coincident {
10459 segments: vec![point0_id.into(), point1_id.into()],
10460 });
10461 let (src_delta, scene_delta) = frontend
10462 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10463 .await
10464 .unwrap();
10465 assert_eq!(
10466 src_delta.text.as_str(),
10467 "\
10468sketch(on = XY) {
10469 line1 = line(start = [var 1, var 2], end = [var 4, var 5])
10470 line2 = line(start = [var 4, var 5], end = [var 7, var 8])
10471 coincident([line1.end, line2.start])
10472}
10473"
10474 );
10475 assert_eq!(
10476 scene_delta.new_graph.objects.len(),
10477 9,
10478 "{:#?}",
10479 scene_delta.new_graph.objects
10480 );
10481
10482 ctx.close().await;
10483 mock_ctx.close().await;
10484 }
10485
10486 #[tokio::test(flavor = "multi_thread")]
10487 async fn test_coincident_of_line_point_and_circle_segment() {
10488 let initial_source = "\
10489sketch(on = XY) {
10490 circle1 = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
10491 line1 = line(start = [var 9mm, var 1mm], end = [var 10mm, var 2mm])
10492}
10493";
10494 let program = Program::parse(initial_source).unwrap().0.unwrap();
10495 let mut frontend = FrontendState::new();
10496
10497 let mock_ctx = ExecutorContext::new_mock(None).await;
10498 let version = Version(0);
10499
10500 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10501 frontend.program = program;
10502 frontend.update_state_after_exec(outcome, true);
10503 let sketch_object = find_first_sketch_object(&frontend.scene_graph).expect("Expected sketch object");
10504 let sketch_id = sketch_object.id;
10505 let sketch = expect_sketch(sketch_object);
10506
10507 let circle_id = sketch
10508 .segments
10509 .iter()
10510 .copied()
10511 .find(|seg_id| {
10512 matches!(
10513 &frontend.scene_graph.objects[seg_id.0].kind,
10514 ObjectKind::Segment {
10515 segment: Segment::Circle(_)
10516 }
10517 )
10518 })
10519 .expect("Expected a circle segment in sketch");
10520 let line_id = frontend
10521 .scene_graph
10522 .objects
10523 .iter()
10524 .find_map(|obj| match &obj.kind {
10525 ObjectKind::Segment {
10526 segment: Segment::Line(line),
10527 } if line.owner.is_none() => Some(obj.id),
10528 _ => None,
10529 })
10530 .expect("Expected a standalone line segment in scene graph");
10531
10532 let line_start_point_id = match &frontend.scene_graph.objects[line_id.0].kind {
10533 ObjectKind::Segment {
10534 segment: Segment::Line(line),
10535 } => line.start,
10536 _ => panic!("Expected line segment object"),
10537 };
10538
10539 let constraint = Constraint::Coincident(Coincident {
10540 segments: vec![line_start_point_id.into(), circle_id.into()],
10541 });
10542 let (src_delta, _scene_delta) = frontend
10543 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10544 .await
10545 .unwrap();
10546 assert_eq!(
10547 src_delta.text.as_str(),
10548 "\
10549sketch(on = XY) {
10550 circle1 = circle(start = [var 7.02mm, var 0mm], center = [var -0.01mm, var 0.22mm])
10551 line1 = line(start = [var 7mm, var 0.78mm], end = [var 10mm, var 2mm])
10552 coincident([line1.start, circle1])
10553}
10554"
10555 );
10556
10557 mock_ctx.close().await;
10558 }
10559
10560 #[tokio::test(flavor = "multi_thread")]
10561 async fn test_invalid_coincident_arc_and_line_preserves_state() {
10562 let program = Program::empty();
10570
10571 let mut frontend = FrontendState::new();
10572 frontend.program = program;
10573
10574 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10575 let mock_ctx = ExecutorContext::new_mock(None).await;
10576 let version = Version(0);
10577
10578 let sketch_args = SketchCtor {
10579 on: Plane::Default(PlaneName::Xy),
10580 };
10581 let (_src_delta, _scene_delta, sketch_id) = frontend
10582 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
10583 .await
10584 .unwrap();
10585
10586 let arc_ctor = ArcCtor {
10588 start: Point2d {
10589 x: Expr::Var(Number {
10590 value: 0.0,
10591 units: NumericSuffix::Mm,
10592 }),
10593 y: Expr::Var(Number {
10594 value: 0.0,
10595 units: NumericSuffix::Mm,
10596 }),
10597 },
10598 end: Point2d {
10599 x: Expr::Var(Number {
10600 value: 10.0,
10601 units: NumericSuffix::Mm,
10602 }),
10603 y: Expr::Var(Number {
10604 value: 10.0,
10605 units: NumericSuffix::Mm,
10606 }),
10607 },
10608 center: Point2d {
10609 x: Expr::Var(Number {
10610 value: 10.0,
10611 units: NumericSuffix::Mm,
10612 }),
10613 y: Expr::Var(Number {
10614 value: 0.0,
10615 units: NumericSuffix::Mm,
10616 }),
10617 },
10618 construction: None,
10619 };
10620 let (_src_delta, scene_delta) = frontend
10621 .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Arc(arc_ctor), None)
10622 .await
10623 .unwrap();
10624 let arc_id = *scene_delta.new_objects.last().unwrap();
10626
10627 let line_ctor = LineCtor {
10629 start: Point2d {
10630 x: Expr::Var(Number {
10631 value: 20.0,
10632 units: NumericSuffix::Mm,
10633 }),
10634 y: Expr::Var(Number {
10635 value: 0.0,
10636 units: NumericSuffix::Mm,
10637 }),
10638 },
10639 end: Point2d {
10640 x: Expr::Var(Number {
10641 value: 30.0,
10642 units: NumericSuffix::Mm,
10643 }),
10644 y: Expr::Var(Number {
10645 value: 10.0,
10646 units: NumericSuffix::Mm,
10647 }),
10648 },
10649 construction: None,
10650 };
10651 let (_src_delta, scene_delta) = frontend
10652 .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Line(line_ctor), None)
10653 .await
10654 .unwrap();
10655 let line_id = *scene_delta.new_objects.last().unwrap();
10657
10658 let constraint = Constraint::Coincident(Coincident {
10661 segments: vec![arc_id.into(), line_id.into()],
10662 });
10663 let result = frontend.add_constraint(&mock_ctx, version, sketch_id, constraint).await;
10664
10665 assert!(result.is_err(), "Expected invalid coincident constraint to fail");
10667
10668 let sketch_object_after =
10671 find_first_sketch_object(&frontend.scene_graph).expect("Sketch should still exist after failed constraint");
10672 let sketch_after = expect_sketch(sketch_object_after);
10673
10674 assert!(
10676 sketch_after.segments.contains(&arc_id),
10677 "Arc segment should still exist after failed constraint"
10678 );
10679 assert!(
10680 sketch_after.segments.contains(&line_id),
10681 "Line segment should still exist after failed constraint"
10682 );
10683
10684 let arc_obj = frontend
10686 .scene_graph
10687 .objects
10688 .get(arc_id.0)
10689 .expect("Arc object should still be accessible");
10690 let line_obj = frontend
10691 .scene_graph
10692 .objects
10693 .get(line_id.0)
10694 .expect("Line object should still be accessible");
10695
10696 match &arc_obj.kind {
10699 ObjectKind::Segment {
10700 segment: Segment::Arc(_),
10701 } => {}
10702 _ => panic!("Arc object should still be an arc segment"),
10703 }
10704 match &line_obj.kind {
10705 ObjectKind::Segment {
10706 segment: Segment::Line(_),
10707 } => {}
10708 _ => panic!("Line object should still be a line segment"),
10709 }
10710
10711 ctx.close().await;
10712 mock_ctx.close().await;
10713 }
10714
10715 #[tokio::test(flavor = "multi_thread")]
10716 async fn test_distance_two_points() {
10717 let initial_source = "\
10718sketch(on = XY) {
10719 point(at = [var 1, var 2])
10720 point(at = [var 3, var 4])
10721}
10722";
10723
10724 let program = Program::parse(initial_source).unwrap().0.unwrap();
10725
10726 let mut frontend = FrontendState::new();
10727
10728 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10729 let mock_ctx = ExecutorContext::new_mock(None).await;
10730 let version = Version(0);
10731
10732 frontend.hack_set_program(&ctx, program).await.unwrap();
10733 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10734 let sketch_id = sketch_object.id;
10735 let sketch = expect_sketch(sketch_object);
10736 let point0_id = *sketch.segments.first().unwrap();
10737 let point1_id = *sketch.segments.get(1).unwrap();
10738
10739 let constraint = Constraint::Distance(Distance {
10740 points: vec![point0_id.into(), point1_id.into()],
10741 distance: Number {
10742 value: 2.0,
10743 units: NumericSuffix::Mm,
10744 },
10745 label_position: None,
10746 source: Default::default(),
10747 });
10748 let (src_delta, scene_delta) = frontend
10749 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10750 .await
10751 .unwrap();
10752 assert_eq!(
10753 src_delta.text.as_str(),
10754 "\
10756sketch(on = XY) {
10757 point1 = point(at = [var 1.29, var 2.29])
10758 point2 = point(at = [var 2.71, var 3.71])
10759 distance([point1, point2]) == 2mm
10760}
10761"
10762 );
10763 assert_eq!(
10764 scene_delta.new_graph.objects.len(),
10765 5,
10766 "{:#?}",
10767 scene_delta.new_graph.objects
10768 );
10769
10770 ctx.close().await;
10771 mock_ctx.close().await;
10772 }
10773
10774 #[tokio::test(flavor = "multi_thread")]
10775 async fn test_distance_two_points_with_label() {
10776 let initial_source = "\
10777sketch(on = XY) {
10778 point(at = [var 1, var 2])
10779 point(at = [var 3, var 4])
10780}
10781";
10782
10783 let program = Program::parse(initial_source).unwrap().0.unwrap();
10784
10785 let mut frontend = FrontendState::new();
10786
10787 let mock_ctx = ExecutorContext::new_mock(None).await;
10788 let version = Version(0);
10789
10790 frontend.program = program.clone();
10791 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10792 frontend.update_state_after_exec(outcome, true);
10793 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10794 let sketch_id = sketch_object.id;
10795 let sketch = expect_sketch(sketch_object);
10796 let point0_id = *sketch.segments.first().unwrap();
10797 let point1_id = *sketch.segments.get(1).unwrap();
10798
10799 let label_position = Point2d {
10800 x: Number {
10801 value: 10.0,
10802 units: NumericSuffix::Mm,
10803 },
10804 y: Number {
10805 value: 11.0,
10806 units: NumericSuffix::Mm,
10807 },
10808 };
10809 let constraint = Constraint::Distance(Distance {
10810 points: vec![point0_id.into(), point1_id.into()],
10811 distance: Number {
10812 value: 2.0,
10813 units: NumericSuffix::Mm,
10814 },
10815 label_position: Some(label_position.clone()),
10816 source: Default::default(),
10817 });
10818 let (src_delta, scene_delta) = frontend
10819 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10820 .await
10821 .unwrap();
10822 assert_eq!(
10823 src_delta.text.as_str(),
10824 "\
10825sketch(on = XY) {
10826 point1 = point(at = [var 1.29, var 2.29])
10827 point2 = point(at = [var 2.71, var 3.71])
10828 distance([point1, point2], labelPosition = [10mm, 11mm]) == 2mm
10829}
10830"
10831 );
10832
10833 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10834 let sketch = expect_sketch(sketch_object);
10835 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10836 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10837 panic!("Expected constraint object");
10838 };
10839 let Constraint::Distance(distance) = constraint else {
10840 panic!("Expected distance constraint");
10841 };
10842 assert_eq!(distance.label_position, Some(label_position));
10843
10844 mock_ctx.close().await;
10845 }
10846
10847 #[tokio::test(flavor = "multi_thread")]
10848 async fn test_edit_distance_constraint_label_position() {
10849 let initial_source = "\
10850sketch(on = XY) {
10851 point(at = [var 1, var 2])
10852 point(at = [var 3, var 2])
10853}
10854";
10855
10856 let program = Program::parse(initial_source).unwrap().0.unwrap();
10857
10858 let mut frontend = FrontendState::new();
10859
10860 let mock_ctx = ExecutorContext::new_mock(None).await;
10861 let version = Version(0);
10862
10863 frontend.program = program.clone();
10864 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10865 frontend.update_state_after_exec(outcome, true);
10866 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10867 let sketch_id = sketch_object.id;
10868 let sketch = expect_sketch(sketch_object);
10869 let point0_id = *sketch.segments.first().unwrap();
10870 let point1_id = *sketch.segments.get(1).unwrap();
10871
10872 let constraint = Constraint::Distance(Distance {
10873 points: vec![point0_id.into(), point1_id.into()],
10874 distance: Number {
10875 value: 2.0,
10876 units: NumericSuffix::Mm,
10877 },
10878 label_position: None,
10879 source: Default::default(),
10880 });
10881 let (_, scene_delta) = frontend
10882 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10883 .await
10884 .unwrap();
10885 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10886 let sketch = expect_sketch(sketch_object);
10887 let constraint_id = sketch.constraints[0];
10888 let label_position = Point2d {
10889 x: Number {
10890 value: 10.0,
10891 units: NumericSuffix::Mm,
10892 },
10893 y: Number {
10894 value: 11.0,
10895 units: NumericSuffix::Mm,
10896 },
10897 };
10898
10899 let (src_delta, scene_delta) = frontend
10900 .edit_distance_constraint_label_position(
10901 &mock_ctx,
10902 version,
10903 sketch_id,
10904 constraint_id,
10905 label_position.clone(),
10906 vec![],
10907 )
10908 .await
10909 .unwrap();
10910 assert_eq!(
10911 src_delta.text.as_str(),
10912 "\
10913sketch(on = XY) {
10914 point1 = point(at = [var 1, var 2])
10915 point2 = point(at = [var 3, var 2])
10916 distance([point1, point2], labelPosition = [10mm, 11mm]) == 2mm
10917}
10918"
10919 );
10920
10921 let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
10922 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10923 panic!("Expected constraint object");
10924 };
10925 let Constraint::Distance(distance) = constraint else {
10926 panic!("Expected distance constraint");
10927 };
10928 assert_eq!(distance.label_position, Some(label_position));
10929
10930 mock_ctx.close().await;
10931 }
10932
10933 #[tokio::test(flavor = "multi_thread")]
10934 async fn test_edit_distance_constraint_label_position_preserves_anchor_segment_solution() {
10935 let initial_source = "\
10936sketch(on = XY) {
10937 point1 = point(at = [var 0mm, var 0mm])
10938 point2 = point(at = [var 10mm, var 0mm])
10939 distance([point1, point2]) == 5mm
10940}
10941";
10942
10943 let program = Program::parse(initial_source).unwrap().0.unwrap();
10944 let mut frontend = FrontendState::new();
10945 let mock_ctx = ExecutorContext::new_mock(None).await;
10946 let version = Version(0);
10947
10948 frontend.program = program.clone();
10949 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10950 frontend.update_state_after_exec(outcome, true);
10951 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10952 let sketch_id = sketch_object.id;
10953 let sketch = expect_sketch(sketch_object);
10954 let point0_id = sketch.segments[0];
10955 let point1_id = sketch.segments[1];
10956 let constraint_id = sketch.constraints[0];
10957
10958 let edited_segments = vec![ExistingSegmentCtor {
10959 id: point0_id,
10960 ctor: SegmentCtor::Point(PointCtor {
10961 position: Point2d {
10962 x: Expr::Var(Number {
10963 value: 2.0,
10964 units: NumericSuffix::Mm,
10965 }),
10966 y: Expr::Var(Number {
10967 value: 1.0,
10968 units: NumericSuffix::Mm,
10969 }),
10970 },
10971 }),
10972 }];
10973 let (_, scene_delta) = frontend
10974 .edit_segments(&mock_ctx, version, sketch_id, edited_segments)
10975 .await
10976 .unwrap();
10977 let point0_after_segment_edit = point_position(&scene_delta.new_graph, point0_id);
10978 let point1_after_segment_edit = point_position(&scene_delta.new_graph, point1_id);
10979
10980 let label_position = Point2d {
10981 x: Number {
10982 value: 3.0,
10983 units: NumericSuffix::Mm,
10984 },
10985 y: Number {
10986 value: 4.0,
10987 units: NumericSuffix::Mm,
10988 },
10989 };
10990 let (_, scene_delta) = frontend
10991 .edit_distance_constraint_label_position(
10992 &mock_ctx,
10993 version,
10994 sketch_id,
10995 constraint_id,
10996 label_position,
10997 vec![point0_id],
10998 )
10999 .await
11000 .unwrap();
11001
11002 assert_point_position_close(
11003 point_position(&scene_delta.new_graph, point0_id),
11004 point0_after_segment_edit,
11005 );
11006 assert_point_position_close(
11007 point_position(&scene_delta.new_graph, point1_id),
11008 point1_after_segment_edit,
11009 );
11010
11011 mock_ctx.close().await;
11012 }
11013
11014 #[tokio::test(flavor = "multi_thread")]
11015 async fn test_distance_point_line() {
11016 let initial_source = "\
11017sketch(on = XY) {
11018 point(at = [var 0, var 5])
11019 line(start = [var 0, var 0], end = [var 10, var 0])
11020}
11021";
11022
11023 let program = Program::parse(initial_source).unwrap().0.unwrap();
11024
11025 let mut frontend = FrontendState::new();
11026
11027 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11028 let mock_ctx = ExecutorContext::new_mock(None).await;
11029 let version = Version(0);
11030
11031 frontend.hack_set_program(&ctx, program).await.unwrap();
11032 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11033 let sketch_id = sketch_object.id;
11034 let sketch = expect_sketch(sketch_object);
11035 let point_id = *sketch.segments.first().unwrap();
11036 let line_id = *sketch
11037 .segments
11038 .iter()
11039 .find(|segment_id| {
11040 matches!(
11041 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11042 Some(ObjectKind::Segment {
11043 segment: Segment::Line(_)
11044 })
11045 )
11046 })
11047 .unwrap();
11048
11049 let label_position = Point2d {
11050 x: Number {
11051 value: 10.0,
11052 units: NumericSuffix::Mm,
11053 },
11054 y: Number {
11055 value: 11.0,
11056 units: NumericSuffix::Mm,
11057 },
11058 };
11059 let constraint = Constraint::Distance(Distance {
11060 points: vec![point_id.into(), line_id.into()],
11061 distance: Number {
11062 value: 5.0,
11063 units: NumericSuffix::Mm,
11064 },
11065 label_position: Some(label_position.clone()),
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 assert_eq!(
11073 src_delta.text.as_str(),
11074 "\
11075sketch(on = XY) {
11076 point1 = point(at = [var 0, var 5])
11077 line1 = line(start = [var 0, var 0], end = [var 10, var 0])
11078 distance([point1, line1], labelPosition = [10mm, 11mm]) == 5mm
11079}
11080"
11081 );
11082 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
11083 let sketch = expect_sketch(sketch_object);
11084 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
11085 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11086 panic!("Expected constraint object");
11087 };
11088 let Constraint::Distance(distance) = constraint else {
11089 panic!("Expected distance constraint");
11090 };
11091 assert_eq!(distance.label_position, Some(label_position));
11092
11093 ctx.close().await;
11094 mock_ctx.close().await;
11095 }
11096
11097 #[tokio::test(flavor = "multi_thread")]
11098 async fn test_distance_point_arc() {
11099 let initial_source = "\
11100sketch(on = XY) {
11101 point(at = [var 0, var 8])
11102 arc(start = [var 5, var 0], end = [var 0, var 5], center = [var 0, var 0])
11103}
11104";
11105
11106 let program = Program::parse(initial_source).unwrap().0.unwrap();
11107
11108 let mut frontend = FrontendState::new();
11109
11110 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11111 let mock_ctx = ExecutorContext::new_mock(None).await;
11112 let version = Version(0);
11113
11114 frontend.hack_set_program(&ctx, program).await.unwrap();
11115 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11116 let sketch_id = sketch_object.id;
11117 let sketch = expect_sketch(sketch_object);
11118 let point_id = *sketch.segments.first().unwrap();
11119 let arc_id = *sketch
11120 .segments
11121 .iter()
11122 .find(|segment_id| {
11123 matches!(
11124 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11125 Some(ObjectKind::Segment {
11126 segment: Segment::Arc(_)
11127 })
11128 )
11129 })
11130 .unwrap();
11131
11132 let constraint = Constraint::Distance(Distance {
11133 points: vec![point_id.into(), arc_id.into()],
11134 distance: Number {
11135 value: 3.0,
11136 units: NumericSuffix::Mm,
11137 },
11138 label_position: None,
11139 source: Default::default(),
11140 });
11141 let (src_delta, _scene_delta) = frontend
11142 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11143 .await
11144 .unwrap();
11145 assert_eq!(
11146 src_delta.text.as_str(),
11147 "\
11148sketch(on = XY) {
11149 point1 = point(at = [var 0, var 8])
11150 arc1 = arc(start = [var 5, var 0], end = [var 0, var 5], center = [var 0, var 0])
11151 distance([point1, arc1]) == 3mm
11152}
11153"
11154 );
11155
11156 ctx.close().await;
11157 mock_ctx.close().await;
11158 }
11159
11160 #[tokio::test(flavor = "multi_thread")]
11161 async fn test_distance_arc_origin() {
11162 let initial_source = "\
11163sketch001 = sketch(on = XY) {
11164 arc(start = [var -4.13mm, var -0.59mm], end = [var -3.47mm, var 3.38mm], center = [var -4.55mm, var 1.52mm])
11165}
11166";
11167
11168 let program = Program::parse(initial_source).unwrap().0.unwrap();
11169
11170 let mut frontend = FrontendState::new();
11171
11172 let mock_ctx = ExecutorContext::new_mock(None).await;
11173 let version = Version(0);
11174
11175 frontend.program = program.clone();
11176 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11177 frontend.update_state_after_exec(outcome, true);
11178 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11179 let sketch_id = sketch_object.id;
11180 let sketch = expect_sketch(sketch_object);
11181 let arc_id = *sketch
11182 .segments
11183 .iter()
11184 .find(|segment_id| {
11185 matches!(
11186 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11187 Some(ObjectKind::Segment {
11188 segment: Segment::Arc(_)
11189 })
11190 )
11191 })
11192 .unwrap();
11193
11194 let constraint = Constraint::Distance(Distance {
11195 points: vec![arc_id.into(), ConstraintSegment::ORIGIN],
11196 distance: Number {
11197 value: 3.0,
11198 units: NumericSuffix::Mm,
11199 },
11200 label_position: None,
11201 source: Default::default(),
11202 });
11203 let (src_delta, _scene_delta) = frontend
11204 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11205 .await
11206 .unwrap();
11207 assert_eq!(
11208 src_delta.text.as_str(),
11209 "\
11210sketch001 = sketch(on = XY) {
11211 arc1 = arc(start = [var -4.16mm, var -0.43mm], end = [var -3.53mm, var 3.28mm], center = [var -4.91mm, var 1.61mm])
11212 distance([arc1, ORIGIN]) == 3mm
11213}
11214"
11215 );
11216
11217 mock_ctx.close().await;
11218 }
11219
11220 #[tokio::test(flavor = "multi_thread")]
11221 async fn test_distance_line_origin() {
11222 let initial_source = "\
11223sketch(on = XY) {
11224 line(start = [var 5, var 0], end = [var 5, var 10])
11225}
11226";
11227
11228 let program = Program::parse(initial_source).unwrap().0.unwrap();
11229
11230 let mut frontend = FrontendState::new();
11231
11232 let mock_ctx = ExecutorContext::new_mock(None).await;
11233 let version = Version(0);
11234
11235 frontend.program = program.clone();
11236 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11237 frontend.update_state_after_exec(outcome, true);
11238 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11239 let sketch_id = sketch_object.id;
11240 let sketch = expect_sketch(sketch_object);
11241 let line_id = *sketch
11242 .segments
11243 .iter()
11244 .find(|segment_id| {
11245 matches!(
11246 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11247 Some(ObjectKind::Segment {
11248 segment: Segment::Line(_)
11249 })
11250 )
11251 })
11252 .unwrap();
11253
11254 let constraint = Constraint::Distance(Distance {
11255 points: vec![ConstraintSegment::ORIGIN, line_id.into()],
11256 distance: Number {
11257 value: 5.0,
11258 units: NumericSuffix::Mm,
11259 },
11260 label_position: None,
11261 source: Default::default(),
11262 });
11263 let (src_delta, _scene_delta) = frontend
11264 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11265 .await
11266 .unwrap();
11267 assert_eq!(
11268 src_delta.text.as_str(),
11269 "\
11270sketch(on = XY) {
11271 line1 = line(start = [var 5, var 0], end = [var 5, var 10])
11272 distance([ORIGIN, line1]) == 5mm
11273}
11274"
11275 );
11276
11277 mock_ctx.close().await;
11278 }
11279
11280 #[tokio::test(flavor = "multi_thread")]
11281 async fn test_distance_line_circle() {
11282 let initial_source = "\
11283sketch(on = XY) {
11284 line(start = [var -10, var 8], end = [var 10, var 8])
11285 circle(start = [var 5, var 0], center = [var 0, var 0])
11286}
11287";
11288
11289 let program = Program::parse(initial_source).unwrap().0.unwrap();
11290
11291 let mut frontend = FrontendState::new();
11292
11293 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11294 let mock_ctx = ExecutorContext::new_mock(None).await;
11295 let version = Version(0);
11296
11297 frontend.hack_set_program(&ctx, program).await.unwrap();
11298 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11299 let sketch_id = sketch_object.id;
11300 let sketch = expect_sketch(sketch_object);
11301 let line_id = *sketch
11302 .segments
11303 .iter()
11304 .find(|segment_id| {
11305 matches!(
11306 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11307 Some(ObjectKind::Segment {
11308 segment: Segment::Line(_)
11309 })
11310 )
11311 })
11312 .unwrap();
11313 let circle_id = *sketch
11314 .segments
11315 .iter()
11316 .find(|segment_id| {
11317 matches!(
11318 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11319 Some(ObjectKind::Segment {
11320 segment: Segment::Circle(_)
11321 })
11322 )
11323 })
11324 .unwrap();
11325
11326 let constraint = Constraint::Distance(Distance {
11327 points: vec![line_id.into(), circle_id.into()],
11328 distance: Number {
11329 value: 3.0,
11330 units: NumericSuffix::Mm,
11331 },
11332 label_position: None,
11333 source: Default::default(),
11334 });
11335 let (src_delta, _scene_delta) = frontend
11336 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11337 .await
11338 .unwrap();
11339 assert_eq!(
11340 src_delta.text.as_str(),
11341 "\
11342sketch(on = XY) {
11343 line1 = line(start = [var -10, var 8], end = [var 10, var 8])
11344 circle1 = circle(start = [var 5, var 0], center = [var 0, var 0])
11345 distance([line1, circle1]) == 3mm
11346}
11347"
11348 );
11349
11350 ctx.close().await;
11351 mock_ctx.close().await;
11352 }
11353
11354 #[tokio::test(flavor = "multi_thread")]
11355 async fn test_distance_circle_arc() {
11356 let initial_source = "\
11357sketch(on = XY) {
11358 circle(start = [var 5, var 0], center = [var 0, var 0])
11359 arc(start = [var 15, var 0], end = [var 10, var 5], center = [var 10, var 0])
11360}
11361";
11362
11363 let program = Program::parse(initial_source).unwrap().0.unwrap();
11364
11365 let mut frontend = FrontendState::new();
11366
11367 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11368 let mock_ctx = ExecutorContext::new_mock(None).await;
11369 let version = Version(0);
11370
11371 frontend.hack_set_program(&ctx, program).await.unwrap();
11372 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11373 let sketch_id = sketch_object.id;
11374 let sketch = expect_sketch(sketch_object);
11375 let circle_id = *sketch
11376 .segments
11377 .iter()
11378 .find(|segment_id| {
11379 matches!(
11380 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11381 Some(ObjectKind::Segment {
11382 segment: Segment::Circle(_)
11383 })
11384 )
11385 })
11386 .unwrap();
11387 let arc_id = *sketch
11388 .segments
11389 .iter()
11390 .find(|segment_id| {
11391 matches!(
11392 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11393 Some(ObjectKind::Segment {
11394 segment: Segment::Arc(_)
11395 })
11396 )
11397 })
11398 .unwrap();
11399
11400 let constraint = Constraint::Distance(Distance {
11401 points: vec![circle_id.into(), arc_id.into()],
11402 distance: Number {
11403 value: 3.0,
11404 units: NumericSuffix::Mm,
11405 },
11406 label_position: None,
11407 source: Default::default(),
11408 });
11409 let (src_delta, _scene_delta) = frontend
11410 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11411 .await
11412 .unwrap();
11413 assert_eq!(
11414 src_delta.text.as_str(),
11415 "\
11416sketch(on = XY) {
11417 circle1 = circle(start = [var 4.33, var 0], center = [var -0.34, var -0.09])
11418 arc1 = arc(start = [var 15.33, var -0.01], end = [var 10.01, var 4.33], center = [var 11.34, var 0.53])
11419 distance([circle1, arc1]) == 3mm
11420}
11421"
11422 );
11423
11424 ctx.close().await;
11425 mock_ctx.close().await;
11426 }
11427
11428 #[tokio::test(flavor = "multi_thread")]
11429 async fn test_distance_parallel_lines() {
11430 let initial_source = "\
11431sketch(on = XY) {
11432 line(start = [var 0, var 0], end = [var 10, var 0])
11433 line(start = [var 0, var 5], end = [var 10, var 5])
11434}
11435";
11436
11437 let program = Program::parse(initial_source).unwrap().0.unwrap();
11438
11439 let mut frontend = FrontendState::new();
11440
11441 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11442 let mock_ctx = ExecutorContext::new_mock(None).await;
11443 let version = Version(0);
11444
11445 frontend.hack_set_program(&ctx, program).await.unwrap();
11446 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11447 let sketch_id = sketch_object.id;
11448 let sketch = expect_sketch(sketch_object);
11449 let line_ids = sketch
11450 .segments
11451 .iter()
11452 .copied()
11453 .filter(|segment_id| {
11454 matches!(
11455 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11456 Some(ObjectKind::Segment {
11457 segment: Segment::Line(_)
11458 })
11459 )
11460 })
11461 .collect::<Vec<_>>();
11462
11463 let constraint = Constraint::Distance(Distance {
11464 points: vec![line_ids[0].into(), line_ids[1].into()],
11465 distance: Number {
11466 value: 5.0,
11467 units: NumericSuffix::Mm,
11468 },
11469 label_position: None,
11470 source: Default::default(),
11471 });
11472 let (src_delta, _scene_delta) = frontend
11473 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11474 .await
11475 .unwrap();
11476 assert_eq!(
11477 src_delta.text.as_str(),
11478 "\
11479sketch(on = XY) {
11480 line1 = line(start = [var 0, var 0], end = [var 10, var 0])
11481 line2 = line(start = [var 0, var 5], end = [var 10, var 5])
11482 distance([line1, line2]) == 5mm
11483}
11484"
11485 );
11486
11487 ctx.close().await;
11488 mock_ctx.close().await;
11489 }
11490
11491 #[tokio::test(flavor = "multi_thread")]
11492 async fn test_distance_non_parallel_lines_lowers_to_distance() {
11493 let initial_source = "\
11494sketch(on = XY) {
11495 line(start = [var 0, var 0], end = [var 10, var 0])
11496 line(start = [var 0, var 0], end = [var 0, var 10])
11497}
11498";
11499
11500 let program = Program::parse(initial_source).unwrap().0.unwrap();
11501
11502 let mut frontend = FrontendState::new();
11503
11504 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11505 let mock_ctx = ExecutorContext::new_mock(None).await;
11506 let version = Version(0);
11507
11508 frontend.hack_set_program(&ctx, program).await.unwrap();
11509 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11510 let sketch_id = sketch_object.id;
11511 let sketch = expect_sketch(sketch_object);
11512 let line_ids = sketch
11513 .segments
11514 .iter()
11515 .copied()
11516 .filter(|segment_id| {
11517 matches!(
11518 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11519 Some(ObjectKind::Segment {
11520 segment: Segment::Line(_)
11521 })
11522 )
11523 })
11524 .collect::<Vec<_>>();
11525
11526 let constraint = Constraint::Distance(Distance {
11527 points: vec![line_ids[0].into(), line_ids[1].into()],
11528 distance: Number {
11529 value: 5.0,
11530 units: NumericSuffix::Mm,
11531 },
11532 label_position: None,
11533 source: Default::default(),
11534 });
11535 let (src_delta, _scene_delta) = frontend
11536 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11537 .await
11538 .unwrap();
11539 assert_eq!(
11540 src_delta.text.as_str(),
11541 "\
11542sketch(on = XY) {
11543 line1 = line(start = [var 4.98, var -0.07], end = [var 4.98, var 0.14])
11544 line2 = line(start = [var 0.02, var 4.3], end = [var 0.03, var 5.65])
11545 distance([line1, line2]) == 5mm
11546}
11547"
11548 );
11549
11550 ctx.close().await;
11551 mock_ctx.close().await;
11552 }
11553
11554 #[tokio::test(flavor = "multi_thread")]
11555 async fn test_horizontal_distance_two_points() {
11556 let initial_source = "\
11557sketch(on = XY) {
11558 point(at = [var 1, var 2])
11559 point(at = [var 3, var 4])
11560}
11561";
11562
11563 let program = Program::parse(initial_source).unwrap().0.unwrap();
11564
11565 let mut frontend = FrontendState::new();
11566
11567 let mock_ctx = ExecutorContext::new_mock(None).await;
11568 let version = Version(0);
11569
11570 frontend.program = program.clone();
11571 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11572 frontend.update_state_after_exec(outcome, true);
11573 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11574 let sketch_id = sketch_object.id;
11575 let sketch = expect_sketch(sketch_object);
11576 let point0_id = *sketch.segments.first().unwrap();
11577 let point1_id = *sketch.segments.get(1).unwrap();
11578 let label_position = Point2d {
11579 x: Number {
11580 value: 10.0,
11581 units: NumericSuffix::Mm,
11582 },
11583 y: Number {
11584 value: 11.0,
11585 units: NumericSuffix::Mm,
11586 },
11587 };
11588
11589 let constraint = Constraint::HorizontalDistance(Distance {
11590 points: vec![point0_id.into(), point1_id.into()],
11591 distance: Number {
11592 value: 2.0,
11593 units: NumericSuffix::Mm,
11594 },
11595 label_position: Some(label_position.clone()),
11596 source: Default::default(),
11597 });
11598 let (src_delta, scene_delta) = frontend
11599 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11600 .await
11601 .unwrap();
11602 assert_eq!(
11603 src_delta.text.as_str(),
11604 "\
11606sketch(on = XY) {
11607 point1 = point(at = [var 1, var 2])
11608 point2 = point(at = [var 3, var 4])
11609 horizontalDistance([point1, point2], labelPosition = [10mm, 11mm]) == 2mm
11610}
11611"
11612 );
11613 assert_eq!(
11614 scene_delta.new_graph.objects.len(),
11615 5,
11616 "{:#?}",
11617 scene_delta.new_graph.objects
11618 );
11619 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
11620 let sketch = expect_sketch(sketch_object);
11621 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
11622 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11623 panic!("Expected constraint object");
11624 };
11625 let Constraint::HorizontalDistance(distance) = constraint else {
11626 panic!("Expected horizontal distance constraint");
11627 };
11628 assert_eq!(distance.label_position, Some(label_position));
11629
11630 mock_ctx.close().await;
11631 }
11632
11633 #[tokio::test(flavor = "multi_thread")]
11634 async fn test_radius_single_arc_segment() {
11635 let initial_source = "\
11636sketch(on = XY) {
11637 arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
11638}
11639";
11640
11641 let program = Program::parse(initial_source).unwrap().0.unwrap();
11642
11643 let mut frontend = FrontendState::new();
11644
11645 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11646 let mock_ctx = ExecutorContext::new_mock(None).await;
11647 let version = Version(0);
11648
11649 frontend.hack_set_program(&ctx, program).await.unwrap();
11650 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11651 let sketch_id = sketch_object.id;
11652 let sketch = expect_sketch(sketch_object);
11653 let arc_id = sketch
11655 .segments
11656 .iter()
11657 .find(|&seg_id| {
11658 let obj = frontend.scene_graph.objects.get(seg_id.0);
11659 matches!(
11660 obj.map(|o| &o.kind),
11661 Some(ObjectKind::Segment {
11662 segment: Segment::Arc(_)
11663 })
11664 )
11665 })
11666 .unwrap();
11667
11668 let constraint = Constraint::Radius(Radius {
11669 arc: *arc_id,
11670 radius: Number {
11671 value: 5.0,
11672 units: NumericSuffix::Mm,
11673 },
11674 label_position: None,
11675 source: Default::default(),
11676 });
11677 let (src_delta, scene_delta) = frontend
11678 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11679 .await
11680 .unwrap();
11681 assert_eq!(
11682 src_delta.text.as_str(),
11683 "\
11685sketch(on = XY) {
11686 arc1 = arc(start = [var 1.83, var 3.62], end = [var 2.42, var 3.3], center = [var -0.25, var -0.92])
11687 radius(arc1) == 5mm
11688}
11689"
11690 );
11691 assert_eq!(
11692 scene_delta.new_graph.objects.len(),
11693 7, "{:#?}",
11695 scene_delta.new_graph.objects
11696 );
11697
11698 ctx.close().await;
11699 mock_ctx.close().await;
11700 }
11701
11702 #[tokio::test(flavor = "multi_thread")]
11703 async fn test_radius_single_arc_segment_with_label_position() {
11704 let initial_source = "\
11705sketch(on = XY) {
11706 arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
11707}
11708";
11709
11710 let program = Program::parse(initial_source).unwrap().0.unwrap();
11711 let mut frontend = FrontendState::new();
11712 let mock_ctx = ExecutorContext::new_mock(None).await;
11713 let version = Version(0);
11714
11715 frontend.program = program.clone();
11716 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11717 frontend.update_state_after_exec(outcome, true);
11718 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11719 let sketch_id = sketch_object.id;
11720 let sketch = expect_sketch(sketch_object);
11721 let arc_id = sketch
11722 .segments
11723 .iter()
11724 .find(|&seg_id| {
11725 let obj = frontend.scene_graph.objects.get(seg_id.0);
11726 matches!(
11727 obj.map(|o| &o.kind),
11728 Some(ObjectKind::Segment {
11729 segment: Segment::Arc(_)
11730 })
11731 )
11732 })
11733 .unwrap();
11734
11735 let label_position = Point2d {
11736 x: Number {
11737 value: 10.0,
11738 units: NumericSuffix::Mm,
11739 },
11740 y: Number {
11741 value: 11.0,
11742 units: NumericSuffix::Mm,
11743 },
11744 };
11745 let constraint = Constraint::Radius(Radius {
11746 arc: *arc_id,
11747 radius: Number {
11748 value: 5.0,
11749 units: NumericSuffix::Mm,
11750 },
11751 label_position: Some(label_position.clone()),
11752 source: Default::default(),
11753 });
11754 let (src_delta, scene_delta) = frontend
11755 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11756 .await
11757 .unwrap();
11758 assert_eq!(
11759 src_delta.text.as_str(),
11760 "\
11761sketch(on = XY) {
11762 arc1 = arc(start = [var 1.83, var 3.62], end = [var 2.42, var 3.3], center = [var -0.25, var -0.92])
11763 radius(arc1, labelPosition = [10mm, 11mm]) == 5mm
11764}
11765"
11766 );
11767
11768 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
11769 let sketch = expect_sketch(sketch_object);
11770 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
11771 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11772 panic!("Expected constraint object");
11773 };
11774 let Constraint::Radius(radius) = constraint else {
11775 panic!("Expected radius constraint");
11776 };
11777 assert_eq!(radius.label_position, Some(label_position));
11778
11779 mock_ctx.close().await;
11780 }
11781
11782 #[tokio::test(flavor = "multi_thread")]
11783 async fn test_edit_radius_constraint_label_position() {
11784 let initial_source = "\
11785sketch(on = XY) {
11786 arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
11787 radius(arc1) == 5mm
11788}
11789";
11790
11791 let program = Program::parse(initial_source).unwrap().0.unwrap();
11792 let mut frontend = FrontendState::new();
11793 let mock_ctx = ExecutorContext::new_mock(None).await;
11794 let version = Version(0);
11795
11796 frontend.program = program.clone();
11797 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11798 frontend.update_state_after_exec(outcome, true);
11799 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11800 let sketch_id = sketch_object.id;
11801 let sketch = expect_sketch(sketch_object);
11802 let constraint_id = sketch.constraints[0];
11803 let label_position = Point2d {
11804 x: Number {
11805 value: 10.0,
11806 units: NumericSuffix::Mm,
11807 },
11808 y: Number {
11809 value: 11.0,
11810 units: NumericSuffix::Mm,
11811 },
11812 };
11813
11814 let (src_delta, scene_delta) = frontend
11815 .edit_distance_constraint_label_position(
11816 &mock_ctx,
11817 version,
11818 sketch_id,
11819 constraint_id,
11820 label_position.clone(),
11821 vec![],
11822 )
11823 .await
11824 .unwrap();
11825 assert_eq!(
11826 src_delta.text.as_str(),
11827 "\
11828sketch(on = XY) {
11829 arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
11830 radius(arc1, labelPosition = [10mm, 11mm]) == 5mm
11831}
11832"
11833 );
11834
11835 let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
11836 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11837 panic!("Expected constraint object");
11838 };
11839 let Constraint::Radius(radius) = constraint else {
11840 panic!("Expected radius constraint");
11841 };
11842 assert_eq!(radius.label_position, Some(label_position));
11843
11844 mock_ctx.close().await;
11845 }
11846
11847 #[tokio::test(flavor = "multi_thread")]
11848 async fn test_vertical_distance_two_points() {
11849 let initial_source = "\
11850sketch(on = XY) {
11851 point(at = [var 1, var 2])
11852 point(at = [var 3, var 4])
11853}
11854";
11855
11856 let program = Program::parse(initial_source).unwrap().0.unwrap();
11857
11858 let mut frontend = FrontendState::new();
11859
11860 let mock_ctx = ExecutorContext::new_mock(None).await;
11861 let version = Version(0);
11862
11863 frontend.program = program.clone();
11864 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11865 frontend.update_state_after_exec(outcome, true);
11866 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11867 let sketch_id = sketch_object.id;
11868 let sketch = expect_sketch(sketch_object);
11869 let point0_id = *sketch.segments.first().unwrap();
11870 let point1_id = *sketch.segments.get(1).unwrap();
11871 let label_position = Point2d {
11872 x: Number {
11873 value: 10.0,
11874 units: NumericSuffix::Mm,
11875 },
11876 y: Number {
11877 value: 11.0,
11878 units: NumericSuffix::Mm,
11879 },
11880 };
11881
11882 let constraint = Constraint::VerticalDistance(Distance {
11883 points: vec![point0_id.into(), point1_id.into()],
11884 distance: Number {
11885 value: 2.0,
11886 units: NumericSuffix::Mm,
11887 },
11888 label_position: Some(label_position.clone()),
11889 source: Default::default(),
11890 });
11891 let (src_delta, scene_delta) = frontend
11892 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11893 .await
11894 .unwrap();
11895 assert_eq!(
11896 src_delta.text.as_str(),
11897 "\
11899sketch(on = XY) {
11900 point1 = point(at = [var 1, var 2])
11901 point2 = point(at = [var 3, var 4])
11902 verticalDistance([point1, point2], labelPosition = [10mm, 11mm]) == 2mm
11903}
11904"
11905 );
11906 assert_eq!(
11907 scene_delta.new_graph.objects.len(),
11908 5,
11909 "{:#?}",
11910 scene_delta.new_graph.objects
11911 );
11912 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
11913 let sketch = expect_sketch(sketch_object);
11914 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
11915 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11916 panic!("Expected constraint object");
11917 };
11918 let Constraint::VerticalDistance(distance) = constraint else {
11919 panic!("Expected vertical distance constraint");
11920 };
11921 assert_eq!(distance.label_position, Some(label_position));
11922
11923 mock_ctx.close().await;
11924 }
11925
11926 #[tokio::test(flavor = "multi_thread")]
11927 async fn test_add_fixed_standalone_point() {
11928 let initial_source = "\
11929sketch(on = XY) {
11930 point(at = [var 1, var 2])
11931}
11932";
11933
11934 let program = Program::parse(initial_source).unwrap().0.unwrap();
11935
11936 let mut frontend = FrontendState::new();
11937
11938 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11939 let mock_ctx = ExecutorContext::new_mock(None).await;
11940 let version = Version(0);
11941
11942 frontend.hack_set_program(&ctx, program).await.unwrap();
11943 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11944 let sketch_id = sketch_object.id;
11945 let sketch = expect_sketch(sketch_object);
11946 let point_id = *sketch.segments.first().unwrap();
11947
11948 let (src_delta, scene_delta) = frontend
11949 .add_constraint(
11950 &mock_ctx,
11951 version,
11952 sketch_id,
11953 Constraint::Fixed(Fixed {
11954 points: vec![FixedPoint {
11955 point: point_id,
11956 position: Point2d {
11957 x: Number {
11958 value: 2.0,
11959 units: NumericSuffix::Mm,
11960 },
11961 y: Number {
11962 value: 3.0,
11963 units: NumericSuffix::Mm,
11964 },
11965 },
11966 }],
11967 }),
11968 )
11969 .await
11970 .unwrap();
11971 assert_eq!(
11972 src_delta.text.as_str(),
11973 "\
11974sketch(on = XY) {
11975 point1 = point(at = [var 2, var 3])
11976 fixed([point1, [2mm, 3mm]])
11977}
11978"
11979 );
11980 assert_eq!(
11981 scene_delta.new_graph.objects.len(),
11982 4,
11983 "{:#?}",
11984 scene_delta.new_graph.objects
11985 );
11986
11987 ctx.close().await;
11988 mock_ctx.close().await;
11989 }
11990
11991 #[tokio::test(flavor = "multi_thread")]
11992 async fn test_add_fixed_multiple_points() {
11993 let initial_source = "\
11994sketch(on = XY) {
11995 point(at = [var 1, var 2])
11996 point(at = [var 3, var 4])
11997}
11998";
11999
12000 let program = Program::parse(initial_source).unwrap().0.unwrap();
12001
12002 let mut frontend = FrontendState::new();
12003
12004 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12005 let mock_ctx = ExecutorContext::new_mock(None).await;
12006 let version = Version(0);
12007
12008 frontend.hack_set_program(&ctx, program).await.unwrap();
12009 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12010 let sketch_id = sketch_object.id;
12011 let sketch = expect_sketch(sketch_object);
12012 let point0_id = *sketch.segments.first().unwrap();
12013 let point1_id = *sketch.segments.get(1).unwrap();
12014
12015 let (src_delta, scene_delta) = frontend
12016 .add_constraint(
12017 &mock_ctx,
12018 version,
12019 sketch_id,
12020 Constraint::Fixed(Fixed {
12021 points: vec![
12022 FixedPoint {
12023 point: point0_id,
12024 position: Point2d {
12025 x: Number {
12026 value: 2.0,
12027 units: NumericSuffix::Mm,
12028 },
12029 y: Number {
12030 value: 3.0,
12031 units: NumericSuffix::Mm,
12032 },
12033 },
12034 },
12035 FixedPoint {
12036 point: point1_id,
12037 position: Point2d {
12038 x: Number {
12039 value: 4.0,
12040 units: NumericSuffix::Mm,
12041 },
12042 y: Number {
12043 value: 5.0,
12044 units: NumericSuffix::Mm,
12045 },
12046 },
12047 },
12048 ],
12049 }),
12050 )
12051 .await
12052 .unwrap();
12053 assert_eq!(
12054 src_delta.text.as_str(),
12055 "\
12056sketch(on = XY) {
12057 point1 = point(at = [var 2, var 3])
12058 point2 = point(at = [var 4, var 5])
12059 fixed([point1, [2mm, 3mm]])
12060 fixed([point2, [4mm, 5mm]])
12061}
12062"
12063 );
12064 assert_eq!(
12065 scene_delta.new_graph.objects.len(),
12066 6,
12067 "{:#?}",
12068 scene_delta.new_graph.objects
12069 );
12070
12071 ctx.close().await;
12072 mock_ctx.close().await;
12073 }
12074
12075 #[tokio::test(flavor = "multi_thread")]
12076 async fn test_add_fixed_owned_point() {
12077 let initial_source = "\
12078sketch(on = XY) {
12079 line(start = [var 1, var 2], end = [var 3, var 4])
12080}
12081";
12082
12083 let program = Program::parse(initial_source).unwrap().0.unwrap();
12084
12085 let mut frontend = FrontendState::new();
12086
12087 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12088 let mock_ctx = ExecutorContext::new_mock(None).await;
12089 let version = Version(0);
12090
12091 frontend.hack_set_program(&ctx, program).await.unwrap();
12092 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12093 let sketch_id = sketch_object.id;
12094 let sketch = expect_sketch(sketch_object);
12095 let line_start_id = *sketch.segments.first().unwrap();
12096
12097 let (src_delta, scene_delta) = frontend
12098 .add_constraint(
12099 &mock_ctx,
12100 version,
12101 sketch_id,
12102 Constraint::Fixed(Fixed {
12103 points: vec![FixedPoint {
12104 point: line_start_id,
12105 position: Point2d {
12106 x: Number {
12107 value: 2.0,
12108 units: NumericSuffix::Mm,
12109 },
12110 y: Number {
12111 value: 3.0,
12112 units: NumericSuffix::Mm,
12113 },
12114 },
12115 }],
12116 }),
12117 )
12118 .await
12119 .unwrap();
12120 assert_eq!(
12121 src_delta.text.as_str(),
12122 "\
12123sketch(on = XY) {
12124 line1 = line(start = [var 2, var 3], end = [var 3, var 4])
12125 fixed([line1.start, [2mm, 3mm]])
12126}
12127"
12128 );
12129 assert_eq!(
12130 scene_delta.new_graph.objects.len(),
12131 6,
12132 "{:#?}",
12133 scene_delta.new_graph.objects
12134 );
12135
12136 ctx.close().await;
12137 mock_ctx.close().await;
12138 }
12139
12140 #[tokio::test(flavor = "multi_thread")]
12141 async fn test_radius_error_cases() {
12142 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12143 let mock_ctx = ExecutorContext::new_mock(None).await;
12144 let version = Version(0);
12145
12146 let initial_source_point = "\
12148sketch(on = XY) {
12149 point(at = [var 1, var 2])
12150}
12151";
12152 let program_point = Program::parse(initial_source_point).unwrap().0.unwrap();
12153 let mut frontend_point = FrontendState::new();
12154 frontend_point.hack_set_program(&ctx, program_point).await.unwrap();
12155 let sketch_object_point = find_first_sketch_object(&frontend_point.scene_graph).unwrap();
12156 let sketch_id_point = sketch_object_point.id;
12157 let sketch_point = expect_sketch(sketch_object_point);
12158 let point_id = *sketch_point.segments.first().unwrap();
12159
12160 let constraint_point = Constraint::Radius(Radius {
12161 arc: point_id,
12162 radius: Number {
12163 value: 5.0,
12164 units: NumericSuffix::Mm,
12165 },
12166 label_position: None,
12167 source: Default::default(),
12168 });
12169 let result_point = frontend_point
12170 .add_constraint(&mock_ctx, version, sketch_id_point, constraint_point)
12171 .await;
12172 assert!(result_point.is_err(), "Single point should error for radius");
12173
12174 let initial_source_line = "\
12176sketch(on = XY) {
12177 line(start = [var 1, var 2], end = [var 3, var 4])
12178}
12179";
12180 let program_line = Program::parse(initial_source_line).unwrap().0.unwrap();
12181 let mut frontend_line = FrontendState::new();
12182 frontend_line.hack_set_program(&ctx, program_line).await.unwrap();
12183 let sketch_object_line = find_first_sketch_object(&frontend_line.scene_graph).unwrap();
12184 let sketch_id_line = sketch_object_line.id;
12185 let sketch_line = expect_sketch(sketch_object_line);
12186 let line_id = *sketch_line.segments.first().unwrap();
12187
12188 let constraint_line = Constraint::Radius(Radius {
12189 arc: line_id,
12190 radius: Number {
12191 value: 5.0,
12192 units: NumericSuffix::Mm,
12193 },
12194 label_position: None,
12195 source: Default::default(),
12196 });
12197 let result_line = frontend_line
12198 .add_constraint(&mock_ctx, version, sketch_id_line, constraint_line)
12199 .await;
12200 assert!(result_line.is_err(), "Single line segment should error for radius");
12201
12202 ctx.close().await;
12203 mock_ctx.close().await;
12204 }
12205
12206 #[tokio::test(flavor = "multi_thread")]
12207 async fn test_diameter_single_arc_segment() {
12208 let initial_source = "\
12209sketch(on = XY) {
12210 arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
12211}
12212";
12213
12214 let program = Program::parse(initial_source).unwrap().0.unwrap();
12215
12216 let mut frontend = FrontendState::new();
12217
12218 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12219 let mock_ctx = ExecutorContext::new_mock(None).await;
12220 let version = Version(0);
12221
12222 frontend.hack_set_program(&ctx, program).await.unwrap();
12223 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12224 let sketch_id = sketch_object.id;
12225 let sketch = expect_sketch(sketch_object);
12226 let arc_id = sketch
12228 .segments
12229 .iter()
12230 .find(|&seg_id| {
12231 let obj = frontend.scene_graph.objects.get(seg_id.0);
12232 matches!(
12233 obj.map(|o| &o.kind),
12234 Some(ObjectKind::Segment {
12235 segment: Segment::Arc(_)
12236 })
12237 )
12238 })
12239 .unwrap();
12240
12241 let constraint = Constraint::Diameter(Diameter {
12242 arc: *arc_id,
12243 diameter: Number {
12244 value: 10.0,
12245 units: NumericSuffix::Mm,
12246 },
12247 label_position: None,
12248 source: Default::default(),
12249 });
12250 let (src_delta, scene_delta) = frontend
12251 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12252 .await
12253 .unwrap();
12254 assert_eq!(
12255 src_delta.text.as_str(),
12256 "\
12258sketch(on = XY) {
12259 arc1 = arc(start = [var 1.83, var 3.62], end = [var 2.42, var 3.3], center = [var -0.25, var -0.92])
12260 diameter(arc1) == 10mm
12261}
12262"
12263 );
12264 assert_eq!(
12265 scene_delta.new_graph.objects.len(),
12266 7, "{:#?}",
12268 scene_delta.new_graph.objects
12269 );
12270
12271 ctx.close().await;
12272 mock_ctx.close().await;
12273 }
12274
12275 #[tokio::test(flavor = "multi_thread")]
12276 async fn test_diameter_single_arc_segment_with_label_position() {
12277 let initial_source = "\
12278sketch(on = XY) {
12279 arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
12280}
12281";
12282
12283 let program = Program::parse(initial_source).unwrap().0.unwrap();
12284 let mut frontend = FrontendState::new();
12285 let mock_ctx = ExecutorContext::new_mock(None).await;
12286 let version = Version(0);
12287
12288 frontend.program = program.clone();
12289 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12290 frontend.update_state_after_exec(outcome, true);
12291 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12292 let sketch_id = sketch_object.id;
12293 let sketch = expect_sketch(sketch_object);
12294 let arc_id = sketch
12295 .segments
12296 .iter()
12297 .find(|&seg_id| {
12298 let obj = frontend.scene_graph.objects.get(seg_id.0);
12299 matches!(
12300 obj.map(|o| &o.kind),
12301 Some(ObjectKind::Segment {
12302 segment: Segment::Arc(_)
12303 })
12304 )
12305 })
12306 .unwrap();
12307
12308 let label_position = Point2d {
12309 x: Number {
12310 value: 10.0,
12311 units: NumericSuffix::Mm,
12312 },
12313 y: Number {
12314 value: 11.0,
12315 units: NumericSuffix::Mm,
12316 },
12317 };
12318 let constraint = Constraint::Diameter(Diameter {
12319 arc: *arc_id,
12320 diameter: Number {
12321 value: 10.0,
12322 units: NumericSuffix::Mm,
12323 },
12324 label_position: Some(label_position.clone()),
12325 source: Default::default(),
12326 });
12327 let (src_delta, scene_delta) = frontend
12328 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12329 .await
12330 .unwrap();
12331 assert_eq!(
12332 src_delta.text.as_str(),
12333 "\
12334sketch(on = XY) {
12335 arc1 = arc(start = [var 1.83, var 3.62], end = [var 2.42, var 3.3], center = [var -0.25, var -0.92])
12336 diameter(arc1, labelPosition = [10mm, 11mm]) == 10mm
12337}
12338"
12339 );
12340
12341 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
12342 let sketch = expect_sketch(sketch_object);
12343 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
12344 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12345 panic!("Expected constraint object");
12346 };
12347 let Constraint::Diameter(diameter) = constraint else {
12348 panic!("Expected diameter constraint");
12349 };
12350 assert_eq!(diameter.label_position, Some(label_position));
12351
12352 mock_ctx.close().await;
12353 }
12354
12355 #[tokio::test(flavor = "multi_thread")]
12356 async fn test_edit_diameter_constraint_label_position() {
12357 let initial_source = "\
12358sketch(on = XY) {
12359 arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
12360 diameter(arc1) == 10mm
12361}
12362";
12363
12364 let program = Program::parse(initial_source).unwrap().0.unwrap();
12365 let mut frontend = FrontendState::new();
12366 let mock_ctx = ExecutorContext::new_mock(None).await;
12367 let version = Version(0);
12368
12369 frontend.program = program.clone();
12370 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12371 frontend.update_state_after_exec(outcome, true);
12372 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12373 let sketch_id = sketch_object.id;
12374 let sketch = expect_sketch(sketch_object);
12375 let constraint_id = sketch.constraints[0];
12376 let label_position = Point2d {
12377 x: Number {
12378 value: 10.0,
12379 units: NumericSuffix::Mm,
12380 },
12381 y: Number {
12382 value: 11.0,
12383 units: NumericSuffix::Mm,
12384 },
12385 };
12386
12387 let (src_delta, scene_delta) = frontend
12388 .edit_distance_constraint_label_position(
12389 &mock_ctx,
12390 version,
12391 sketch_id,
12392 constraint_id,
12393 label_position.clone(),
12394 vec![],
12395 )
12396 .await
12397 .unwrap();
12398 assert_eq!(
12399 src_delta.text.as_str(),
12400 "\
12401sketch(on = XY) {
12402 arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
12403 diameter(arc1, labelPosition = [10mm, 11mm]) == 10mm
12404}
12405"
12406 );
12407
12408 let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
12409 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12410 panic!("Expected constraint object");
12411 };
12412 let Constraint::Diameter(diameter) = constraint else {
12413 panic!("Expected diameter constraint");
12414 };
12415 assert_eq!(diameter.label_position, Some(label_position));
12416
12417 mock_ctx.close().await;
12418 }
12419
12420 #[tokio::test(flavor = "multi_thread")]
12421 async fn test_diameter_error_cases() {
12422 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12423 let mock_ctx = ExecutorContext::new_mock(None).await;
12424 let version = Version(0);
12425
12426 let initial_source_point = "\
12428sketch(on = XY) {
12429 point(at = [var 1, var 2])
12430}
12431";
12432 let program_point = Program::parse(initial_source_point).unwrap().0.unwrap();
12433 let mut frontend_point = FrontendState::new();
12434 frontend_point.hack_set_program(&ctx, program_point).await.unwrap();
12435 let sketch_object_point = find_first_sketch_object(&frontend_point.scene_graph).unwrap();
12436 let sketch_id_point = sketch_object_point.id;
12437 let sketch_point = expect_sketch(sketch_object_point);
12438 let point_id = *sketch_point.segments.first().unwrap();
12439
12440 let constraint_point = Constraint::Diameter(Diameter {
12441 arc: point_id,
12442 diameter: Number {
12443 value: 10.0,
12444 units: NumericSuffix::Mm,
12445 },
12446 label_position: None,
12447 source: Default::default(),
12448 });
12449 let result_point = frontend_point
12450 .add_constraint(&mock_ctx, version, sketch_id_point, constraint_point)
12451 .await;
12452 assert!(result_point.is_err(), "Single point should error for diameter");
12453
12454 let initial_source_line = "\
12456sketch(on = XY) {
12457 line(start = [var 1, var 2], end = [var 3, var 4])
12458}
12459";
12460 let program_line = Program::parse(initial_source_line).unwrap().0.unwrap();
12461 let mut frontend_line = FrontendState::new();
12462 frontend_line.hack_set_program(&ctx, program_line).await.unwrap();
12463 let sketch_object_line = find_first_sketch_object(&frontend_line.scene_graph).unwrap();
12464 let sketch_id_line = sketch_object_line.id;
12465 let sketch_line = expect_sketch(sketch_object_line);
12466 let line_id = *sketch_line.segments.first().unwrap();
12467
12468 let constraint_line = Constraint::Diameter(Diameter {
12469 arc: line_id,
12470 diameter: Number {
12471 value: 10.0,
12472 units: NumericSuffix::Mm,
12473 },
12474 label_position: None,
12475 source: Default::default(),
12476 });
12477 let result_line = frontend_line
12478 .add_constraint(&mock_ctx, version, sketch_id_line, constraint_line)
12479 .await;
12480 assert!(result_line.is_err(), "Single line segment should error for diameter");
12481
12482 ctx.close().await;
12483 mock_ctx.close().await;
12484 }
12485
12486 #[tokio::test(flavor = "multi_thread")]
12487 async fn test_line_horizontal() {
12488 let initial_source = "\
12489sketch(on = XY) {
12490 line(start = [var 1, var 2], end = [var 3, var 4])
12491}
12492";
12493
12494 let program = Program::parse(initial_source).unwrap().0.unwrap();
12495
12496 let mut frontend = FrontendState::new();
12497
12498 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12499 let mock_ctx = ExecutorContext::new_mock(None).await;
12500 let version = Version(0);
12501
12502 frontend.hack_set_program(&ctx, program).await.unwrap();
12503 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12504 let sketch_id = sketch_object.id;
12505 let sketch = expect_sketch(sketch_object);
12506 let line1_id = *sketch.segments.get(2).unwrap();
12507
12508 let constraint = Constraint::Horizontal(Horizontal::Line { line: line1_id });
12509 let (src_delta, scene_delta) = frontend
12510 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12511 .await
12512 .unwrap();
12513 assert_eq!(
12514 src_delta.text.as_str(),
12515 "\
12516sketch(on = XY) {
12517 line1 = line(start = [var 1, var 3], end = [var 3, var 3])
12518 horizontal(line1)
12519}
12520"
12521 );
12522 assert_eq!(
12523 scene_delta.new_graph.objects.len(),
12524 6,
12525 "{:#?}",
12526 scene_delta.new_graph.objects
12527 );
12528
12529 ctx.close().await;
12530 mock_ctx.close().await;
12531 }
12532
12533 #[tokio::test(flavor = "multi_thread")]
12534 async fn test_control_point_spline_edge_horizontal() {
12535 let initial_source = "\
12536@settings(experimentalFeatures = allow)
12537splineSketch = sketch(on = XY) {
12538 controlPointSpline1 = controlPointSpline(points = [
12539 [var 0mm, var 0mm],
12540 [var 10mm, var 20mm],
12541 [var 20mm, var 0mm],
12542 ])
12543}
12544";
12545
12546 let program = Program::parse(initial_source).unwrap().0.unwrap();
12547
12548 let mut frontend = FrontendState::new();
12549
12550 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12551 let mock_ctx = ExecutorContext::new_mock(None).await;
12552 let version = Version(0);
12553
12554 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
12555 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12556 let sketch_id = sketch_object.id;
12557 let sketch = expect_sketch(sketch_object);
12558 let spline_id = sketch
12559 .segments
12560 .iter()
12561 .copied()
12562 .find(|seg_id| {
12563 matches!(
12564 &frontend.scene_graph.objects[seg_id.0].kind,
12565 ObjectKind::Segment {
12566 segment: Segment::ControlPointSpline(_)
12567 }
12568 )
12569 })
12570 .expect("Expected a control point spline segment in sketch");
12571 let edge_id = frontend
12572 .scene_graph
12573 .objects
12574 .iter()
12575 .find_map(|obj| match &obj.kind {
12576 ObjectKind::Segment {
12577 segment: Segment::Line(line),
12578 } if line.owner == Some(spline_id) => Some(obj.id),
12579 _ => None,
12580 })
12581 .expect("Expected an owned control-polygon edge");
12582
12583 let constraint = Constraint::Horizontal(Horizontal::Line { line: edge_id });
12584 let (src_delta, _) = frontend
12585 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12586 .await
12587 .unwrap();
12588 assert!(
12589 src_delta.text.contains("horizontal(controlPointSpline1.edges[0])"),
12590 "Expected horizontal constraint on spline edge, got: {}",
12591 src_delta.text
12592 );
12593
12594 ctx.close().await;
12595 mock_ctx.close().await;
12596 }
12597
12598 #[tokio::test(flavor = "multi_thread")]
12599 async fn test_control_point_spline_edge_angle() {
12600 let initial_source = "\
12601@settings(experimentalFeatures = allow)
12602splineSketch = sketch(on = XY) {
12603 controlPointSpline1 = controlPointSpline(points = [
12604 [var 0mm, var 0mm],
12605 [var 10mm, var 20mm],
12606 [var 20mm, var 0mm],
12607 ])
12608
12609 line1 = line(start = [var 40mm, var 0mm], end = [var 60mm, var 10mm])
12610}
12611";
12612
12613 let program = Program::parse(initial_source).unwrap().0.unwrap();
12614
12615 let mut frontend = FrontendState::new();
12616
12617 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12618 let mock_ctx = ExecutorContext::new_mock(None).await;
12619 let version = Version(0);
12620
12621 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
12622 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12623 let sketch_id = sketch_object.id;
12624 let sketch = expect_sketch(sketch_object);
12625 let spline_id = sketch
12626 .segments
12627 .iter()
12628 .copied()
12629 .find(|seg_id| {
12630 matches!(
12631 &frontend.scene_graph.objects[seg_id.0].kind,
12632 ObjectKind::Segment {
12633 segment: Segment::ControlPointSpline(_)
12634 }
12635 )
12636 })
12637 .expect("Expected a control point spline segment in sketch");
12638 let edge_id = frontend
12639 .scene_graph
12640 .objects
12641 .iter()
12642 .find_map(|obj| match &obj.kind {
12643 ObjectKind::Segment {
12644 segment: Segment::Line(line),
12645 } if line.owner == Some(spline_id) => Some(obj.id),
12646 _ => None,
12647 })
12648 .expect("Expected an owned control-polygon edge");
12649 let line1_id = frontend
12650 .scene_graph
12651 .objects
12652 .iter()
12653 .find_map(|obj| match &obj.kind {
12654 ObjectKind::Segment {
12655 segment: Segment::Line(line),
12656 } if line.owner.is_none() && obj.label == "line1" => Some(obj.id),
12657 _ => None,
12658 })
12659 .or_else(|| {
12660 sketch.segments.iter().copied().find(|seg_id| {
12661 matches!(
12662 &frontend.scene_graph.objects[seg_id.0].kind,
12663 ObjectKind::Segment {
12664 segment: Segment::Line(line),
12665 } if line.owner.is_none()
12666 )
12667 })
12668 })
12669 .expect("Expected a standalone line segment in sketch");
12670
12671 let constraint = Constraint::Angle(Angle {
12672 lines: vec![line1_id, edge_id],
12673 angle: Number {
12674 value: 30.0,
12675 units: NumericSuffix::Deg,
12676 },
12677 source: Default::default(),
12678 });
12679 let (src_delta, _) = frontend
12680 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12681 .await
12682 .unwrap();
12683 assert!(
12684 src_delta
12685 .text
12686 .contains("angle([line1, controlPointSpline1.edges[0]]) == 30deg"),
12687 "Expected angle constraint on spline edge, got: {}",
12688 src_delta.text
12689 );
12690
12691 ctx.close().await;
12692 mock_ctx.close().await;
12693 }
12694
12695 #[tokio::test(flavor = "multi_thread")]
12696 async fn test_ui_scene_graph_hides_same_spline_coincident_constraints() {
12697 let initial_source = "\
12698@settings(experimentalFeatures = allow)
12699splineSketch = sketch(on = XY) {
12700 spline1 = controlPointSpline(points = [
12701 [var 0mm, var 0mm],
12702 [var 10mm, var 20mm],
12703 [var 20mm, var 0mm],
12704 ])
12705 line1 = line(start = [var 0mm, var 0mm], end = [var -10mm, var 0mm])
12706 coincident([spline1.controls[1], spline1.edges[0]])
12707 coincident([spline1.controls[0], line1])
12708}
12709";
12710
12711 let program = Program::parse(initial_source).unwrap().0.unwrap();
12712
12713 let mut frontend = FrontendState::new();
12714
12715 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12716 let mock_ctx = ExecutorContext::new_mock(None).await;
12717
12718 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
12719
12720 let ui_scene_graph = frontend.scene_graph_for_ui();
12721 let sketch_object = find_first_sketch_object(&ui_scene_graph).unwrap();
12722 let sketch = expect_sketch(sketch_object);
12723
12724 assert_eq!(
12725 sketch.constraints.len(),
12726 1,
12727 "Expected only the external coincident constraint to remain visible in the UI scene graph"
12728 );
12729
12730 let visible_constraints = ui_scene_graph
12731 .objects
12732 .iter()
12733 .filter_map(|object| match &object.kind {
12734 ObjectKind::Constraint {
12735 constraint: Constraint::Coincident(coincident),
12736 } => Some(coincident.clone()),
12737 _ => None,
12738 })
12739 .collect::<Vec<_>>();
12740
12741 assert_eq!(
12742 visible_constraints.len(),
12743 1,
12744 "Expected only one coincident constraint object in the UI scene graph"
12745 );
12746 assert_eq!(
12747 visible_constraints[0].get_segments().len(),
12748 2,
12749 "Expected the remaining visible coincident constraint to reference two segments"
12750 );
12751
12752 ctx.close().await;
12753 mock_ctx.close().await;
12754 }
12755
12756 #[tokio::test(flavor = "multi_thread")]
12757 async fn test_edit_control_point_spline_can_append_control_point() {
12758 let initial_source = "\
12759@settings(experimentalFeatures = allow)
12760splineSketch = sketch(on = XY) {
12761 controlPointSpline(points = [
12762 [var 0mm, var 0mm],
12763 [var 10mm, var 20mm],
12764 [var 20mm, var 0mm],
12765 ])
12766}
12767";
12768
12769 let program = Program::parse(initial_source).unwrap().0.unwrap();
12770
12771 let mut frontend = FrontendState::new();
12772
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 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
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 spline_id = sketch
12782 .segments
12783 .iter()
12784 .copied()
12785 .find(|seg_id| {
12786 matches!(
12787 &frontend.scene_graph.objects[seg_id.0].kind,
12788 ObjectKind::Segment {
12789 segment: Segment::ControlPointSpline(_)
12790 }
12791 )
12792 })
12793 .expect("Expected a control point spline segment in sketch");
12794
12795 let ctor = ControlPointSplineCtor {
12796 points: vec![
12797 Point2d {
12798 x: Expr::Var(Number {
12799 value: 0.0,
12800 units: NumericSuffix::Mm,
12801 }),
12802 y: Expr::Var(Number {
12803 value: 0.0,
12804 units: NumericSuffix::Mm,
12805 }),
12806 },
12807 Point2d {
12808 x: Expr::Var(Number {
12809 value: 10.0,
12810 units: NumericSuffix::Mm,
12811 }),
12812 y: Expr::Var(Number {
12813 value: 20.0,
12814 units: NumericSuffix::Mm,
12815 }),
12816 },
12817 Point2d {
12818 x: Expr::Var(Number {
12819 value: 20.0,
12820 units: NumericSuffix::Mm,
12821 }),
12822 y: Expr::Var(Number {
12823 value: 0.0,
12824 units: NumericSuffix::Mm,
12825 }),
12826 },
12827 Point2d {
12828 x: Expr::Var(Number {
12829 value: 30.0,
12830 units: NumericSuffix::Mm,
12831 }),
12832 y: Expr::Var(Number {
12833 value: 10.0,
12834 units: NumericSuffix::Mm,
12835 }),
12836 },
12837 ],
12838 construction: None,
12839 };
12840
12841 let segments = vec![ExistingSegmentCtor {
12842 id: spline_id,
12843 ctor: SegmentCtor::ControlPointSpline(ctor),
12844 }];
12845 let (src_delta, scene_delta) = frontend
12846 .edit_segments(&mock_ctx, version, sketch_id, segments)
12847 .await
12848 .unwrap();
12849
12850 assert!(
12851 src_delta.text.contains("[var 30mm, var 10mm]"),
12852 "Expected appended spline control point in source, got: {}",
12853 src_delta.text
12854 );
12855
12856 assert!(
12857 scene_delta.invalidates_ids,
12858 "Expected appending a spline control point to invalidate ids"
12859 );
12860 let updated_spline = scene_delta
12861 .new_graph
12862 .objects
12863 .iter()
12864 .find_map(|obj| match &obj.kind {
12865 ObjectKind::Segment {
12866 segment: Segment::ControlPointSpline(updated_spline),
12867 } if updated_spline.controls.len() == 4 => Some(updated_spline),
12868 _ => None,
12869 })
12870 .expect("Expected edited scene graph to contain a four-point control point spline");
12871 assert_eq!(
12872 updated_spline.controls.len(),
12873 4,
12874 "Expected edited spline to expose four control points"
12875 );
12876
12877 ctx.close().await;
12878 mock_ctx.close().await;
12879 }
12880
12881 #[tokio::test(flavor = "multi_thread")]
12882 async fn test_line_vertical() {
12883 let initial_source = "\
12884sketch(on = XY) {
12885 line(start = [var 1, var 2], end = [var 3, var 4])
12886}
12887";
12888
12889 let program = Program::parse(initial_source).unwrap().0.unwrap();
12890
12891 let mut frontend = FrontendState::new();
12892
12893 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12894 let mock_ctx = ExecutorContext::new_mock(None).await;
12895 let version = Version(0);
12896
12897 frontend.hack_set_program(&ctx, program).await.unwrap();
12898 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12899 let sketch_id = sketch_object.id;
12900 let sketch = expect_sketch(sketch_object);
12901 let line1_id = *sketch.segments.get(2).unwrap();
12902
12903 let constraint = Constraint::Vertical(Vertical::Line { line: line1_id });
12904 let (src_delta, scene_delta) = frontend
12905 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12906 .await
12907 .unwrap();
12908 assert_eq!(
12909 src_delta.text.as_str(),
12910 "\
12911sketch(on = XY) {
12912 line1 = line(start = [var 2, var 2], end = [var 2, var 4])
12913 vertical(line1)
12914}
12915"
12916 );
12917 assert_eq!(
12918 scene_delta.new_graph.objects.len(),
12919 6,
12920 "{:#?}",
12921 scene_delta.new_graph.objects
12922 );
12923
12924 ctx.close().await;
12925 mock_ctx.close().await;
12926 }
12927
12928 #[tokio::test(flavor = "multi_thread")]
12929 async fn test_points_vertical() {
12930 let initial_source = "\
12931sketch001 = sketch(on = XY) {
12932 p0 = point(at = [var -2.23mm, var 3.1mm])
12933 pf = point(at = [4, 4])
12934}
12935";
12936
12937 let program = Program::parse(initial_source).unwrap().0.unwrap();
12938
12939 let mut frontend = FrontendState::new();
12940
12941 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12942 let mock_ctx = ExecutorContext::new_mock(None).await;
12943 let version = Version(0);
12944
12945 frontend.hack_set_program(&ctx, program).await.unwrap();
12946 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12947 let sketch_id = sketch_object.id;
12948 let sketch = expect_sketch(sketch_object);
12949 let point_ids = vec![
12950 sketch.segments.first().unwrap().to_owned(),
12951 sketch.segments.get(1).unwrap().to_owned(),
12952 ];
12953
12954 let constraint = Constraint::Vertical(Vertical::Points {
12955 points: point_ids.into_iter().map(ConstraintSegment::from).collect(),
12956 });
12957 let (src_delta, scene_delta) = frontend
12958 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12959 .await
12960 .unwrap();
12961 assert_eq!(
12962 src_delta.text.as_str(),
12963 "\
12964sketch001 = sketch(on = XY) {
12965 p0 = point(at = [var 4mm, var 3.1mm])
12966 pf = point(at = [4, 4])
12967 vertical([p0, pf])
12968}
12969"
12970 );
12971 assert_eq!(
12972 scene_delta.new_graph.objects.len(),
12973 5,
12974 "{:#?}",
12975 scene_delta.new_graph.objects
12976 );
12977
12978 ctx.close().await;
12979 mock_ctx.close().await;
12980 }
12981
12982 #[tokio::test(flavor = "multi_thread")]
12983 async fn test_points_horizontal() {
12984 let initial_source = "\
12985sketch001 = sketch(on = XY) {
12986 p0 = point(at = [var -2.23mm, var 3.1mm])
12987 pf = point(at = [4, 4])
12988}
12989";
12990
12991 let program = Program::parse(initial_source).unwrap().0.unwrap();
12992
12993 let mut frontend = FrontendState::new();
12994
12995 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12996 let mock_ctx = ExecutorContext::new_mock(None).await;
12997 let version = Version(0);
12998
12999 frontend.hack_set_program(&ctx, program).await.unwrap();
13000 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13001 let sketch_id = sketch_object.id;
13002 let sketch = expect_sketch(sketch_object);
13003 let point_ids = vec![
13004 sketch.segments.first().unwrap().to_owned(),
13005 sketch.segments.get(1).unwrap().to_owned(),
13006 ];
13007
13008 let constraint = Constraint::Horizontal(Horizontal::Points {
13009 points: point_ids.into_iter().map(ConstraintSegment::from).collect(),
13010 });
13011 let (src_delta, scene_delta) = frontend
13012 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13013 .await
13014 .unwrap();
13015 assert_eq!(
13016 src_delta.text.as_str(),
13017 "\
13018sketch001 = sketch(on = XY) {
13019 p0 = point(at = [var -2.23mm, var 4mm])
13020 pf = point(at = [4, 4])
13021 horizontal([p0, pf])
13022}
13023"
13024 );
13025 assert_eq!(
13026 scene_delta.new_graph.objects.len(),
13027 5,
13028 "{:#?}",
13029 scene_delta.new_graph.objects
13030 );
13031
13032 ctx.close().await;
13033 mock_ctx.close().await;
13034 }
13035
13036 #[tokio::test(flavor = "multi_thread")]
13037 async fn test_point_horizontal_with_origin() {
13038 let initial_source = "\
13039sketch001 = sketch(on = XY) {
13040 p0 = point(at = [var -2.23mm, var 3.1mm])
13041}
13042";
13043
13044 let program = Program::parse(initial_source).unwrap().0.unwrap();
13045
13046 let mut frontend = FrontendState::new();
13047
13048 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13049 let mock_ctx = ExecutorContext::new_mock(None).await;
13050 let version = Version(0);
13051
13052 frontend.hack_set_program(&ctx, program).await.unwrap();
13053 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13054 let sketch_id = sketch_object.id;
13055 let sketch = expect_sketch(sketch_object);
13056 let point_id = *sketch.segments.first().unwrap();
13057
13058 let constraint = Constraint::Horizontal(Horizontal::Points {
13059 points: vec![ConstraintSegment::from(point_id), ConstraintSegment::ORIGIN],
13060 });
13061 let (src_delta, scene_delta) = frontend
13062 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13063 .await
13064 .unwrap();
13065 assert_eq!(
13066 src_delta.text.as_str(),
13067 "\
13068sketch001 = sketch(on = XY) {
13069 p0 = point(at = [var -2.23mm, var 0mm])
13070 horizontal([p0, ORIGIN])
13071}
13072"
13073 );
13074 assert_eq!(
13075 scene_delta.new_graph.objects.len(),
13076 4,
13077 "{:#?}",
13078 scene_delta.new_graph.objects
13079 );
13080
13081 ctx.close().await;
13082 mock_ctx.close().await;
13083 }
13084
13085 #[tokio::test(flavor = "multi_thread")]
13086 async fn test_lines_equal_length() {
13087 let initial_source = "\
13088sketch(on = XY) {
13089 line(start = [var 1, var 2], end = [var 3, var 4])
13090 line(start = [var 5, var 6], end = [var 7, var 8])
13091}
13092";
13093
13094 let program = Program::parse(initial_source).unwrap().0.unwrap();
13095
13096 let mut frontend = FrontendState::new();
13097
13098 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13099 let mock_ctx = ExecutorContext::new_mock(None).await;
13100 let version = Version(0);
13101
13102 frontend.hack_set_program(&ctx, program).await.unwrap();
13103 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13104 let sketch_id = sketch_object.id;
13105 let sketch = expect_sketch(sketch_object);
13106 let line1_id = *sketch.segments.get(2).unwrap();
13107 let line2_id = *sketch.segments.get(5).unwrap();
13108
13109 let constraint = Constraint::LinesEqualLength(LinesEqualLength {
13110 lines: vec![line1_id, line2_id],
13111 });
13112 let (src_delta, scene_delta) = frontend
13113 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13114 .await
13115 .unwrap();
13116 assert_eq!(
13117 src_delta.text.as_str(),
13118 "\
13119sketch(on = XY) {
13120 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
13121 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
13122 equalLength([line1, line2])
13123}
13124"
13125 );
13126 assert_eq!(
13127 scene_delta.new_graph.objects.len(),
13128 9,
13129 "{:#?}",
13130 scene_delta.new_graph.objects
13131 );
13132
13133 ctx.close().await;
13134 mock_ctx.close().await;
13135 }
13136
13137 #[tokio::test(flavor = "multi_thread")]
13138 async fn test_add_constraint_multi_line_equal_length() {
13139 let initial_source = "\
13140sketch(on = XY) {
13141 line(start = [var 1, var 2], end = [var 3, var 4])
13142 line(start = [var 5, var 6], end = [var 7, var 8])
13143 line(start = [var 9, var 10], end = [var 11, var 12])
13144}
13145";
13146
13147 let program = Program::parse(initial_source).unwrap().0.unwrap();
13148
13149 let mut frontend = FrontendState::new();
13150 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13151 let mock_ctx = ExecutorContext::new_mock(None).await;
13152 let version = Version(0);
13153
13154 frontend.hack_set_program(&ctx, program).await.unwrap();
13155 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13156 let sketch_id = sketch_object.id;
13157 let sketch = expect_sketch(sketch_object);
13158 let line1_id = *sketch.segments.get(2).unwrap();
13159 let line2_id = *sketch.segments.get(5).unwrap();
13160 let line3_id = *sketch.segments.get(8).unwrap();
13161
13162 let constraint = Constraint::LinesEqualLength(LinesEqualLength {
13163 lines: vec![line1_id, line2_id, line3_id],
13164 });
13165 let (src_delta, scene_delta) = frontend
13166 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13167 .await
13168 .unwrap();
13169 assert_eq!(
13170 src_delta.text.as_str(),
13171 "\
13172sketch(on = XY) {
13173 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
13174 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
13175 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
13176 equalLength([line1, line2, line3])
13177}
13178"
13179 );
13180 let constraints = scene_delta
13181 .new_graph
13182 .objects
13183 .iter()
13184 .filter_map(|obj| {
13185 let ObjectKind::Constraint { constraint } = &obj.kind else {
13186 return None;
13187 };
13188 Some(constraint)
13189 })
13190 .collect::<Vec<_>>();
13191
13192 assert_eq!(constraints.len(), 1, "{:#?}", frontend.scene_graph.objects);
13193 let Constraint::LinesEqualLength(lines_equal_length) = constraints[0] else {
13194 panic!("expected equal length constraint, got {:?}", constraints[0]);
13195 };
13196 assert_eq!(lines_equal_length.lines.len(), 3);
13197
13198 ctx.close().await;
13199 mock_ctx.close().await;
13200 }
13201
13202 #[tokio::test(flavor = "multi_thread")]
13203 async fn test_lines_parallel() {
13204 let initial_source = "\
13205sketch(on = XY) {
13206 line(start = [var 1, var 2], end = [var 3, var 4])
13207 line(start = [var 5, var 6], end = [var 7, var 8])
13208}
13209";
13210
13211 let program = Program::parse(initial_source).unwrap().0.unwrap();
13212
13213 let mut frontend = FrontendState::new();
13214
13215 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13216 let mock_ctx = ExecutorContext::new_mock(None).await;
13217 let version = Version(0);
13218
13219 frontend.hack_set_program(&ctx, program).await.unwrap();
13220 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13221 let sketch_id = sketch_object.id;
13222 let sketch = expect_sketch(sketch_object);
13223 let line1_id = *sketch.segments.get(2).unwrap();
13224 let line2_id = *sketch.segments.get(5).unwrap();
13225
13226 let constraint = Constraint::Parallel(Parallel {
13227 lines: vec![line1_id, line2_id],
13228 });
13229 let (src_delta, scene_delta) = frontend
13230 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13231 .await
13232 .unwrap();
13233 assert_eq!(
13234 src_delta.text.as_str(),
13235 "\
13236sketch(on = XY) {
13237 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
13238 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
13239 parallel([line1, line2])
13240}
13241"
13242 );
13243 assert_eq!(
13244 scene_delta.new_graph.objects.len(),
13245 9,
13246 "{:#?}",
13247 scene_delta.new_graph.objects
13248 );
13249
13250 ctx.close().await;
13251 mock_ctx.close().await;
13252 }
13253
13254 #[tokio::test(flavor = "multi_thread")]
13255 async fn test_lines_parallel_multiline() {
13256 let initial_source = "\
13257sketch(on = XY) {
13258 line(start = [var 1, var 2], end = [var 3, var 4])
13259 line(start = [var 5, var 6], end = [var 7, var 8])
13260 line(start = [var 9, var 10], end = [var 11, var 12])
13261}
13262";
13263
13264 let program = Program::parse(initial_source).unwrap().0.unwrap();
13265
13266 let mut frontend = FrontendState::new();
13267
13268 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13269 let mock_ctx = ExecutorContext::new_mock(None).await;
13270 let version = Version(0);
13271
13272 frontend.hack_set_program(&ctx, program).await.unwrap();
13273 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13274 let sketch_id = sketch_object.id;
13275 let sketch = expect_sketch(sketch_object);
13276 let line1_id = *sketch.segments.get(2).unwrap();
13277 let line2_id = *sketch.segments.get(5).unwrap();
13278 let line3_id = *sketch.segments.get(8).unwrap();
13279
13280 let constraint = Constraint::Parallel(Parallel {
13281 lines: vec![line1_id, line2_id, line3_id],
13282 });
13283 let (src_delta, scene_delta) = frontend
13284 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13285 .await
13286 .unwrap();
13287 assert_eq!(
13288 src_delta.text.as_str(),
13289 "\
13290sketch(on = XY) {
13291 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
13292 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
13293 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
13294 parallel([line1, line2, line3])
13295}
13296"
13297 );
13298
13299 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
13300 let sketch = expect_sketch(sketch_object);
13301 assert_eq!(sketch.constraints.len(), 1);
13302
13303 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
13304 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
13305 panic!("Expected constraint object");
13306 };
13307 let Constraint::Parallel(parallel) = constraint else {
13308 panic!("Expected parallel constraint");
13309 };
13310 assert_eq!(parallel.lines.len(), 3);
13311
13312 ctx.close().await;
13313 mock_ctx.close().await;
13314 }
13315
13316 #[tokio::test(flavor = "multi_thread")]
13317 async fn test_lines_perpendicular() {
13318 let initial_source = "\
13319sketch(on = XY) {
13320 line(start = [var 1, var 2], end = [var 3, var 4])
13321 line(start = [var 5, var 6], end = [var 7, var 8])
13322}
13323";
13324
13325 let program = Program::parse(initial_source).unwrap().0.unwrap();
13326
13327 let mut frontend = FrontendState::new();
13328
13329 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13330 let mock_ctx = ExecutorContext::new_mock(None).await;
13331 let version = Version(0);
13332
13333 frontend.hack_set_program(&ctx, program).await.unwrap();
13334 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13335 let sketch_id = sketch_object.id;
13336 let sketch = expect_sketch(sketch_object);
13337 let line1_id = *sketch.segments.get(2).unwrap();
13338 let line2_id = *sketch.segments.get(5).unwrap();
13339
13340 let constraint = Constraint::Perpendicular(Perpendicular {
13341 lines: vec![line1_id, line2_id],
13342 });
13343 let (src_delta, scene_delta) = frontend
13344 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13345 .await
13346 .unwrap();
13347 assert_eq!(
13348 src_delta.text.as_str(),
13349 "\
13350sketch(on = XY) {
13351 line1 = line(start = [var 2, var 3], end = [var 2, var 3])
13352 line2 = line(start = [var 6, var 7], end = [var 6, var 7])
13353 perpendicular([line1, line2])
13354}
13355"
13356 );
13357 assert_eq!(
13358 scene_delta.new_graph.objects.len(),
13359 9,
13360 "{:#?}",
13361 scene_delta.new_graph.objects
13362 );
13363
13364 ctx.close().await;
13365 mock_ctx.close().await;
13366 }
13367
13368 #[tokio::test(flavor = "multi_thread")]
13369 async fn test_lines_angle() {
13370 let initial_source = "\
13371sketch(on = XY) {
13372 line(start = [var 1, var 2], end = [var 3, var 4])
13373 line(start = [var 5, var 6], end = [var 7, var 8])
13374}
13375";
13376
13377 let program = Program::parse(initial_source).unwrap().0.unwrap();
13378
13379 let mut frontend = FrontendState::new();
13380
13381 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13382 let mock_ctx = ExecutorContext::new_mock(None).await;
13383 let version = Version(0);
13384
13385 frontend.hack_set_program(&ctx, program).await.unwrap();
13386 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13387 let sketch_id = sketch_object.id;
13388 let sketch = expect_sketch(sketch_object);
13389 let line1_id = *sketch.segments.get(2).unwrap();
13390 let line2_id = *sketch.segments.get(5).unwrap();
13391
13392 let constraint = Constraint::Angle(Angle {
13393 lines: vec![line1_id, line2_id],
13394 angle: Number {
13395 value: 30.0,
13396 units: NumericSuffix::Deg,
13397 },
13398 source: Default::default(),
13399 });
13400 let (src_delta, scene_delta) = frontend
13401 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13402 .await
13403 .unwrap();
13404 assert_eq!(
13405 src_delta.text.as_str(),
13406 "\
13408sketch(on = XY) {
13409 line1 = line(start = [var 0.9, var 2.36], end = [var 3.1, var 3.64])
13410 line2 = line(start = [var 5.36, var 5.9], end = [var 6.64, var 8.1])
13411 angle([line1, line2]) == 30deg
13412}
13413"
13414 );
13415 assert_eq!(
13416 scene_delta.new_graph.objects.len(),
13417 9,
13418 "{:#?}",
13419 scene_delta.new_graph.objects
13420 );
13421
13422 ctx.close().await;
13423 mock_ctx.close().await;
13424 }
13425
13426 #[tokio::test(flavor = "multi_thread")]
13427 async fn test_segments_tangent() {
13428 let initial_source = "\
13429sketch(on = XY) {
13430 line(start = [var 1, var 2], end = [var 3, var 4])
13431 arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
13432}
13433";
13434
13435 let program = Program::parse(initial_source).unwrap().0.unwrap();
13436
13437 let mut frontend = FrontendState::new();
13438
13439 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13440 let mock_ctx = ExecutorContext::new_mock(None).await;
13441 let version = Version(0);
13442
13443 frontend.hack_set_program(&ctx, program).await.unwrap();
13444 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13445 let sketch_id = sketch_object.id;
13446 let sketch = expect_sketch(sketch_object);
13447 let line1_id = *sketch.segments.get(2).unwrap();
13448 let arc1_id = *sketch.segments.get(6).unwrap();
13449
13450 let constraint = Constraint::Tangent(Tangent {
13451 input: vec![line1_id, arc1_id],
13452 });
13453 let (src_delta, scene_delta) = frontend
13454 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13455 .await
13456 .unwrap();
13457 assert_eq!(
13458 src_delta.text.as_str(),
13459 "\
13460sketch(on = XY) {
13461 line1 = line(start = [var 0.84, var 2.13], end = [var 3.82, var 3.27])
13462 arc1 = arc(start = [var 4.51, var 2.03], end = [var 7.05, var 2.02], center = [var 5.78, var 2.55])
13463 tangent([line1, arc1])
13464}
13465"
13466 );
13467 assert_eq!(
13468 scene_delta.new_graph.objects.len(),
13469 10,
13470 "{:#?}",
13471 scene_delta.new_graph.objects
13472 );
13473
13474 ctx.close().await;
13475 mock_ctx.close().await;
13476 }
13477
13478 #[tokio::test(flavor = "multi_thread")]
13479 async fn test_point_midpoint() {
13480 let initial_source = "\
13481sketch(on = XY) {
13482 point(at = [var 1, var 1])
13483 line(start = [var 0, var 0], end = [var 6, var 4])
13484}
13485";
13486
13487 let program = Program::parse(initial_source).unwrap().0.unwrap();
13488
13489 let mut frontend = FrontendState::new();
13490
13491 let ctx = ExecutorContext::new_mock(None).await;
13492 let version = Version(0);
13493
13494 frontend.program = program.clone();
13495 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
13496 frontend.update_state_after_exec(outcome, true);
13497 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13498 let sketch_id = sketch_object.id;
13499 let sketch = expect_sketch(sketch_object);
13500 let point_id = *sketch.segments.first().unwrap();
13501 let line_id = *sketch.segments.get(3).unwrap();
13502
13503 let constraint = Constraint::Midpoint(Midpoint {
13504 point: ConstraintSegment::from(point_id),
13505 segment: line_id,
13506 });
13507 let (src_delta, scene_delta) = frontend
13508 .add_constraint(&ctx, version, sketch_id, constraint)
13509 .await
13510 .unwrap();
13511 assert_eq!(
13512 src_delta.text.as_str(),
13513 "\
13514sketch(on = XY) {
13515 point1 = point(at = [var 2.33, var 1.67])
13516 line1 = line(start = [var -0.67, var -0.33], end = [var 5.33, var 3.67])
13517 midpoint(line1, point = point1)
13518}
13519"
13520 );
13521 assert_eq!(
13522 scene_delta.new_graph.objects.len(),
13523 7,
13524 "{:#?}",
13525 scene_delta.new_graph.objects
13526 );
13527
13528 ctx.close().await;
13529 }
13530
13531 #[tokio::test(flavor = "multi_thread")]
13532 async fn test_segments_symmetric() {
13533 let initial_source = "\
13534sketch(on = XY) {
13535 line(start = [var 0, var 0], end = [var 0, var 4])
13536 line(start = [var 4, var 0], end = [var 4, var 4])
13537 line(start = [var 2, var -1], end = [var 2, var 5])
13538}
13539";
13540
13541 let program = Program::parse(initial_source).unwrap().0.unwrap();
13542
13543 let mut frontend = FrontendState::new();
13544
13545 let ctx = ExecutorContext::new_mock(None).await;
13546 let version = Version(0);
13547
13548 frontend.program = program.clone();
13549 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
13550 frontend.update_state_after_exec(outcome, true);
13551 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13552 let sketch_id = sketch_object.id;
13553 let sketch = expect_sketch(sketch_object);
13554 let line1_id = *sketch.segments.get(2).unwrap();
13555 let line2_id = *sketch.segments.get(5).unwrap();
13556 let axis_id = *sketch.segments.get(8).unwrap();
13557
13558 let constraint = Constraint::Symmetric(Symmetric {
13559 input: vec![line1_id, line2_id],
13560 axis: axis_id,
13561 });
13562 let (src_delta, scene_delta) = frontend
13563 .add_constraint(&ctx, version, sketch_id, constraint)
13564 .await
13565 .unwrap();
13566 assert_eq!(
13567 src_delta.text.as_str(),
13568 "\
13569sketch(on = XY) {
13570 line1 = line(start = [var 0, var 0], end = [var 0, var 4])
13571 line2 = line(start = [var 4, var 0], end = [var 4, var 4])
13572 line3 = line(start = [var 2, var -1], end = [var 2, var 5])
13573 symmetric([line1, line2], axis = line3)
13574}
13575"
13576 );
13577 assert_eq!(
13578 scene_delta.new_graph.objects.len(),
13579 12,
13580 "{:#?}",
13581 scene_delta.new_graph.objects
13582 );
13583
13584 ctx.close().await;
13585 }
13586
13587 #[tokio::test(flavor = "multi_thread")]
13588 async fn test_point_arc_midpoint() {
13589 let initial_source = "\
13590sketch(on = XY) {
13591 point(at = [var 6, var 3])
13592 arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
13593}
13594";
13595
13596 let program = Program::parse(initial_source).unwrap().0.unwrap();
13597
13598 let mut frontend = FrontendState::new();
13599
13600 let ctx = ExecutorContext::new_mock(None).await;
13601 let version = Version(0);
13602
13603 frontend.program = program.clone();
13604 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
13605 frontend.update_state_after_exec(outcome, true);
13606 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13607 let sketch_id = sketch_object.id;
13608 let sketch = expect_sketch(sketch_object);
13609 let point_id = *sketch.segments.first().unwrap();
13610 let arc_id = *sketch.segments.get(4).unwrap();
13611
13612 let constraint = Constraint::Midpoint(Midpoint {
13613 point: ConstraintSegment::from(point_id),
13614 segment: arc_id,
13615 });
13616 let (src_delta, scene_delta) = frontend
13617 .add_constraint(&ctx, version, sketch_id, constraint)
13618 .await
13619 .unwrap();
13620 assert_eq!(
13621 src_delta.text.as_str(),
13622 "\
13623sketch(on = XY) {
13624 point1 = point(at = [var 6, var 2.35])
13625 arc1 = arc(start = [var 6, var 2.35], end = [var 6, var 2.35], center = [var 6, var 1.94])
13626 midpoint(arc1, point = point1)
13627}
13628"
13629 );
13630 assert_eq!(
13631 scene_delta.new_graph.objects.len(),
13632 8,
13633 "{:#?}",
13634 scene_delta.new_graph.objects
13635 );
13636
13637 ctx.close().await;
13638 }
13639
13640 #[tokio::test(flavor = "multi_thread")]
13641 async fn test_origin_line_midpoint() {
13642 let initial_source = "\
13643sketch(on = XY) {
13644 line(start = [var 0, var 0], end = [var 6, var 4])
13645}
13646";
13647
13648 let program = Program::parse(initial_source).unwrap().0.unwrap();
13649
13650 let mut frontend = FrontendState::new();
13651
13652 let ctx = ExecutorContext::new_mock(None).await;
13653 let version = Version(0);
13654
13655 frontend.program = program.clone();
13656 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
13657 frontend.update_state_after_exec(outcome, true);
13658 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13659 let sketch_id = sketch_object.id;
13660 let sketch = expect_sketch(sketch_object);
13661 let line_id = *sketch.segments.get(2).unwrap();
13662
13663 let constraint = Constraint::Midpoint(Midpoint {
13664 point: ConstraintSegment::ORIGIN,
13665 segment: line_id,
13666 });
13667 let (src_delta, scene_delta) = frontend
13668 .add_constraint(&ctx, version, sketch_id, constraint)
13669 .await
13670 .unwrap();
13671 assert_eq!(
13672 src_delta.text.as_str(),
13673 "\
13674sketch(on = XY) {
13675 line1 = line(start = [var -3, var -2], end = [var 3, var 2])
13676 midpoint(line1, point = ORIGIN)
13677}
13678"
13679 );
13680 assert_eq!(
13681 scene_delta.new_graph.objects.len(),
13682 6,
13683 "{:#?}",
13684 scene_delta.new_graph.objects
13685 );
13686
13687 ctx.close().await;
13688 }
13689
13690 #[tokio::test(flavor = "multi_thread")]
13691 async fn test_origin_arc_midpoint() {
13692 let initial_source = "\
13693sketch(on = XY) {
13694 arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
13695}
13696";
13697
13698 let program = Program::parse(initial_source).unwrap().0.unwrap();
13699
13700 let mut frontend = FrontendState::new();
13701
13702 let ctx = ExecutorContext::new_mock(None).await;
13703 let version = Version(0);
13704
13705 frontend.program = program.clone();
13706 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
13707 frontend.update_state_after_exec(outcome, true);
13708 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13709 let sketch_id = sketch_object.id;
13710 let sketch = expect_sketch(sketch_object);
13711 let arc_id = *sketch.segments.get(3).unwrap();
13712
13713 let constraint = Constraint::Midpoint(Midpoint {
13714 point: ConstraintSegment::ORIGIN,
13715 segment: arc_id,
13716 });
13717 let (src_delta, scene_delta) = frontend
13718 .add_constraint(&ctx, version, sketch_id, constraint)
13719 .await
13720 .unwrap();
13721 assert_eq!(
13722 src_delta.text.as_str(),
13723 "\
13724sketch(on = XY) {
13725 arc1 = arc(start = [var 0.35, var 2.24], end = [var 1.62, var -1.58], center = [var 2.34, var 0.78])
13726 midpoint(arc1, point = ORIGIN)
13727}
13728"
13729 );
13730 assert_eq!(
13731 scene_delta.new_graph.objects.len(),
13732 7,
13733 "{:#?}",
13734 scene_delta.new_graph.objects
13735 );
13736
13737 ctx.close().await;
13738 }
13739
13740 #[tokio::test(flavor = "multi_thread")]
13741 async fn test_segments_symmetric_arcs() {
13742 let initial_source = "\
13743sketch(on = XY) {
13744 arc(start = [var -15, var 0], end = [var -10, var 5], center = [var -10, var 0])
13745 arc(start = [var 6, var 2], end = [var 12, var -4], center = [var 8, var 1])
13746 line(start = [var 0, var -10], end = [var 0, var 10])
13747}
13748";
13749
13750 let program = Program::parse(initial_source).unwrap().0.unwrap();
13751
13752 let mut frontend = FrontendState::new();
13753
13754 let ctx = ExecutorContext::new_mock(None).await;
13755 let version = Version(0);
13756
13757 frontend.program = program.clone();
13758 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
13759 frontend.update_state_after_exec(outcome, true);
13760 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13761 let sketch_id = sketch_object.id;
13762 let sketch = expect_sketch(sketch_object);
13763 let arc1_id = *sketch.segments.get(3).unwrap();
13764 let arc2_id = *sketch.segments.get(7).unwrap();
13765 let axis_id = *sketch.segments.get(10).unwrap();
13766
13767 let constraint = Constraint::Symmetric(Symmetric {
13768 input: vec![arc1_id, arc2_id],
13769 axis: axis_id,
13770 });
13771 let (src_delta, scene_delta) = frontend
13772 .add_constraint(&ctx, version, sketch_id, constraint)
13773 .await
13774 .unwrap();
13775 assert_eq!(
13776 src_delta.text.as_str(),
13777 "\
13778sketch(on = XY) {
13779 arc1 = arc(start = [var -14.46, var 0], end = [var -10, var 4.65], center = [var -10.14, var 0.31])
13780 arc2 = arc(start = [var 5.49, var 2.26], end = [var 11.58, var -3.47], center = [var 9.34, var 0.25])
13781 line1 = line(start = [var -0.44, var -10], end = [var -0.37, var 10])
13782 symmetric([arc1, arc2], axis = line1)
13783}
13784"
13785 );
13786 assert_eq!(
13787 scene_delta.new_graph.objects.len(),
13788 14,
13789 "{:#?}",
13790 scene_delta.new_graph.objects
13791 );
13792
13793 ctx.close().await;
13794 }
13795
13796 #[tokio::test(flavor = "multi_thread")]
13797 async fn test_sketch_on_face_simple() {
13798 let initial_source = "\
13799len = 2mm
13800cube = startSketchOn(XY)
13801 |> startProfile(at = [0, 0])
13802 |> line(end = [len, 0], tag = $side)
13803 |> line(end = [0, len])
13804 |> line(end = [-len, 0])
13805 |> line(end = [0, -len])
13806 |> close()
13807 |> extrude(length = len)
13808
13809face = faceOf(cube, face = side)
13810";
13811
13812 let program = Program::parse(initial_source).unwrap().0.unwrap();
13813
13814 let mut frontend = FrontendState::new();
13815
13816 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13817 let mock_ctx = ExecutorContext::new_mock(None).await;
13818 let version = Version(0);
13819
13820 frontend.hack_set_program(&ctx, program).await.unwrap();
13821 let face_object = find_first_face_object(&frontend.scene_graph).unwrap();
13822 let face_id = face_object.id;
13823
13824 let sketch_args = SketchCtor {
13825 on: Plane::Object(face_id),
13826 };
13827 let (_src_delta, scene_delta, sketch_id) = frontend
13828 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
13829 .await
13830 .unwrap();
13831 assert_eq!(sketch_id, ObjectId(2));
13832 assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
13833 let sketch_object = &scene_delta.new_graph.objects[2];
13834 assert_eq!(sketch_object.id, ObjectId(2));
13835 assert_eq!(
13836 sketch_object.kind,
13837 ObjectKind::Sketch(Sketch {
13838 args: SketchCtor {
13839 on: Plane::Object(face_id),
13840 },
13841 plane: face_id,
13842 segments: vec![],
13843 constraints: vec![],
13844 })
13845 );
13846 assert_eq!(scene_delta.new_graph.objects.len(), 8);
13847
13848 ctx.close().await;
13849 mock_ctx.close().await;
13850 }
13851
13852 #[tokio::test(flavor = "multi_thread")]
13853 async fn test_sketch_on_wall_artifact_from_region_extrude() {
13854 let initial_source = "\
13855s = sketch(on = YZ) {
13856 line1 = line(start = [0, 0], end = [0, 1])
13857 line2 = line(start = [0, 1], end = [1, 1])
13858 line3 = line(start = [1, 1], end = [0, 0])
13859}
13860region001 = region(point = [0.1, 0.1], sketch = s)
13861extrude001 = extrude(region001, length = 5)
13862";
13863
13864 let program = Program::parse(initial_source).unwrap().0.unwrap();
13865
13866 let mut frontend = FrontendState::new();
13867 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13868 let version = Version(0);
13869
13870 frontend.hack_set_program(&ctx, program).await.unwrap();
13871 let wall_object_id = find_first_wall_object_id(&frontend.scene_graph).expect("expected a wall object");
13872
13873 let sketch_args = SketchCtor {
13874 on: Plane::Object(wall_object_id),
13875 };
13876 let (src_delta, _scene_delta, _sketch_id) = frontend
13877 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
13878 .await
13879 .unwrap();
13880 assert!(src_delta.text.contains("faceOf(extrude001, face = region001.tags."));
13881
13882 ctx.close().await;
13883 }
13884
13885 #[tokio::test(flavor = "multi_thread")]
13886 async fn test_sketch_on_wall_artifact_from_split_region_extrude() {
13887 let initial_source = "\
13888sketch001 = sketch(on = YZ) {
13889 line1 = line(start = [var 0.49, var -0.39], end = [var 6.52, var -0.39])
13890 line2 = line(start = [var 6.52, var -0.39], end = [var 6.52, var 4.9])
13891 line3 = line(start = [var 6.52, var 4.9], end = [var 0.49, var 4.9])
13892 line4 = line(start = [var 0.49, var 4.9], end = [var 0.49, var -0.39])
13893 coincident([line1.end, line2.start])
13894 coincident([line2.end, line3.start])
13895 coincident([line3.end, line4.start])
13896 coincident([line4.end, line1.start])
13897 parallel([line2, line4])
13898 parallel([line3, line1])
13899 perpendicular([line1, line2])
13900 horizontal(line3)
13901 line5 = line(start = [2.35, 6.65], end = [5.89, -2.7])
13902}
13903region001 = region(point = [3.1, 3.74], sketch = sketch001)
13904extrude001 = extrude(region001, length = 5)
13905";
13906
13907 let program = Program::parse(initial_source).unwrap().0.unwrap();
13908
13909 let mut frontend = FrontendState::new();
13910 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13911 let version = Version(0);
13912
13913 frontend.hack_set_program(&ctx, program).await.unwrap();
13914 let wall_object_id = find_first_wall_object_id(&frontend.scene_graph).expect("expected a wall object");
13915
13916 let sketch_args = SketchCtor {
13917 on: Plane::Object(wall_object_id),
13918 };
13919 let (src_delta, _scene_delta, _sketch_id) = frontend
13920 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
13921 .await
13922 .unwrap();
13923 assert!(src_delta.text.contains("faceOf(extrude001, face = region001.tags."));
13924
13925 ctx.close().await;
13926 }
13927
13928 #[tokio::test(flavor = "multi_thread")]
13929 async fn test_new_sketch_on_multi_region_extrude_cap_indexes_selected_solid() {
13930 let initial_source = "\
13931@settings(kclVersion = 2.0)
13932
13933sketch001 = sketch(on = XY) {
13934 circle1 = circle(start = [var -1.51mm, var 1.31mm], center = [var -1.98mm, var 1.03mm])
13935 circle2 = circle(start = [var 2.98mm, var 2.37mm], center = [var 2.66mm, var 1.46mm])
13936}
13937hidden001 = hide(sketch001)
13938region001 = region(segments = [sketch001.circle2])
13939region002 = region(segments = [sketch001.circle1])
13940extrude001 = extrude([region001, region002], length = 5)
13941";
13942
13943 let program = Program::parse(initial_source).unwrap().0.unwrap();
13944 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13945 let version = Version(0);
13946
13947 for (solid_output_index, expected_face) in [
13948 (0, "faceOf(extrude001[0], face = END)"),
13949 (1, "faceOf(extrude001[1], face = END)"),
13950 ] {
13951 let mut frontend = FrontendState::new();
13952 frontend.hack_set_program(&ctx, program.clone()).await.unwrap();
13953 let cap_object_id = find_cap_object_id_with_solid_output_index(
13954 &frontend.scene_graph,
13955 crate::frontend::api::CapKind::End,
13956 solid_output_index,
13957 )
13958 .unwrap_or_else(|| panic!("expected an end cap object for solid output index {solid_output_index}"));
13959
13960 let sketch_args = SketchCtor {
13961 on: Plane::Object(cap_object_id),
13962 };
13963 let (src_delta, _scene_delta, _sketch_id) = frontend
13964 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
13965 .await
13966 .unwrap();
13967
13968 assert!(
13969 src_delta.text.contains(expected_face),
13970 "expected `{expected_face}` in:\n{}",
13971 src_delta.text
13972 );
13973 assert!(!src_delta.text.contains("faceOf(extrude001, face = END)"));
13974 }
13975
13976 ctx.close().await;
13977 }
13978
13979 #[tokio::test(flavor = "multi_thread")]
13980 async fn test_new_sketch_on_multi_region_extrude_wall_indexes_selected_solid() {
13981 let initial_source = "\
13982@settings(kclVersion = 2.0)
13983
13984sketch001 = sketch(on = XY) {
13985 rect1Line1 = line(start = [0, 0], end = [1, 0])
13986 rect1Line2 = line(start = [1, 0], end = [1, 1])
13987 rect1Line3 = line(start = [1, 1], end = [0, 1])
13988 rect1Line4 = line(start = [0, 1], end = [0, 0])
13989 rect2Line1 = line(start = [3, 0], end = [4, 0])
13990 rect2Line2 = line(start = [4, 0], end = [4, 1])
13991 rect2Line3 = line(start = [4, 1], end = [3, 1])
13992 rect2Line4 = line(start = [3, 1], end = [3, 0])
13993}
13994hidden001 = hide(sketch001)
13995region001 = region(segments = [
13996 sketch001.rect1Line4,
13997 sketch001.rect1Line1
13998])
13999region002 = region(segments = [
14000 sketch001.rect2Line4,
14001 sketch001.rect2Line1
14002])
14003extrude001 = extrude([region001, region002], length = 5)
14004";
14005
14006 let program = Program::parse(initial_source).unwrap().0.unwrap();
14007 let mut frontend = FrontendState::new();
14008 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14009 let version = Version(0);
14010
14011 frontend.hack_set_program(&ctx, program).await.unwrap();
14012 let region_call = "\
14013region(segments = [
14014 sketch001.rect1Line4,
14015 sketch001.rect1Line1
14016])";
14017 let region_call_start = initial_source.find(region_call).unwrap();
14018 let region_range = [region_call_start, region_call_start + region_call.len(), 0].into();
14019 let segment_call = "line(start = [0, 0], end = [1, 0])";
14020 let segment_call_start = initial_source.find(segment_call).unwrap();
14021 let segment_range = [segment_call_start, segment_call_start + segment_call.len(), 0].into();
14022 let wall_object_id = frontend
14023 .scene_graph
14024 .objects
14025 .iter()
14026 .find_map(|object| match &object.kind {
14027 ObjectKind::Wall(wall)
14028 if wall.source.path.as_ref().is_some_and(|path| path.range == region_range)
14029 && wall.source.segment.range == segment_range =>
14030 {
14031 Some(object.id)
14032 }
14033 _ => None,
14034 })
14035 .expect("expected a wall object for region001.tags.rect1Line1");
14036
14037 let sketch_args = SketchCtor {
14038 on: Plane::Object(wall_object_id),
14039 };
14040 let (src_delta, _scene_delta, _sketch_id) = frontend
14041 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14042 .await
14043 .unwrap();
14044
14045 let expected_face = "faceOf(extrude001[0], face = region001.tags.rect1Line1)";
14046 assert!(
14047 src_delta.text.contains(expected_face),
14048 "expected `{expected_face}` in:\n{}",
14049 src_delta.text
14050 );
14051 assert!(!src_delta.text.contains("faceOf(extrude001, face ="));
14052
14053 ctx.close().await;
14054 }
14055
14056 #[test]
14057 fn test_enclosing_variable_fallback_skips_nested_sketch_items() {
14058 let source = "\
14059sketch001 = sketch(on = XY) {
14060 line(start = [0, 0], end = [1, 0])
14061}
14062part = subtract(boxSolid, tools = [cutSolid])
14063 |> appearance(color = \"#8f96a3\")
14064";
14065 let ast = Program::parse(source).unwrap().0.unwrap().ast;
14066 let line_start = source.find("line").unwrap();
14067 let line_end = line_start + "line(start = [0, 0], end = [1, 0])".len();
14068 let line_ref = SourceRef::Simple {
14069 range: [line_start, line_end, 0].into(),
14070 node_path: None,
14071 };
14072 assert_eq!(variable_name_containing_source_ref(&ast, &line_ref), None);
14073
14074 let subtract_start = source.find("subtract").unwrap();
14075 let subtract_end = subtract_start + "subtract(boxSolid, tools = [cutSolid])".len();
14076 let subtract_ref = SourceRef::Simple {
14077 range: [subtract_start, subtract_end, 0].into(),
14078 node_path: None,
14079 };
14080 assert_eq!(
14081 variable_name_containing_source_ref(&ast, &subtract_ref),
14082 Some("part".to_owned())
14083 );
14084 }
14085
14086 #[tokio::test(flavor = "multi_thread")]
14087 async fn test_sketch_on_subtracted_sweep_cap_uses_composite_solid() {
14088 clear_mem_cache().await;
14089 let source = "\
14090boxSolid = startSketchOn(XY)
14091 |> startProfile(at = [0, 0])
14092 |> line(end = [4, 0], tag = $bottomEdge)
14093 |> line(end = [0, 4])
14094 |> line(end = [-4, 0])
14095 |> close()
14096 |> extrude(length = 10)
14097cutSolid = startSketchOn(XY)
14098 |> startProfile(at = [1, 1])
14099 |> line(end = [1, 0])
14100 |> line(end = [0, 1])
14101 |> line(end = [-1, 0])
14102 |> close()
14103 |> extrude(length = 10)
14104part = subtract(boxSolid, tools = [cutSolid])
14105 |> appearance(color = \"#8f96a3\", roughness = 55, metalness = 8)
14106";
14107 let program = Program::parse(source).unwrap().0.unwrap();
14108 let mut frontend = FrontendState::new();
14109 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14110 match frontend.hack_set_program(&ctx, program).await.unwrap() {
14111 SetProgramOutcome::Success { .. } => {}
14112 SetProgramOutcome::ExecFailure { error } => panic!("KCL fixture failed to execute: {error:?}"),
14113 }
14114
14115 let sweep_call_start = source.find("extrude").unwrap();
14116 let sweep_call_end = sweep_call_start + "extrude(length = 10)".len();
14117 let part_call_start = source.find("subtract").unwrap();
14118 let part_call_end = part_call_start + "subtract(boxSolid, tools = [cutSolid])".len();
14119 let sweep_range = [sweep_call_start, sweep_call_end, 0].into();
14120 let composite_range = [part_call_start, part_call_end, 0].into();
14121
14122 let cap_object = frontend
14123 .scene_graph
14124 .objects
14125 .iter()
14126 .find(|object| {
14127 matches!(
14128 &object.kind,
14129 ObjectKind::Cap(crate::frontend::api::Cap {
14130 kind: crate::frontend::api::CapKind::End,
14131 source,
14132 ..
14133 }) if source.solid.range == composite_range && source.sweep.range == sweep_range
14134 )
14135 })
14136 .expect("expected end cap object to trace through subtract and original extrude");
14137
14138 let mut ast = frontend.program.ast.clone();
14139 let cap_expr = sketch_on_ast_expr(&mut ast, &frontend.scene_graph, &Plane::Object(cap_object.id)).unwrap();
14140 let cap_face_decl = ast::VariableDeclaration::new(
14141 ast::VariableDeclarator::new("capFace", cap_expr.clone()),
14142 ast::ItemVisibility::Default,
14143 ast::VariableKind::Const,
14144 );
14145 ast.body
14146 .push(ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(
14147 cap_face_decl,
14148 ))));
14149 let generated_source = source_from_ast(&ast);
14150
14151 assert!(generated_source.contains("capFace = faceOf(part, face = END)"));
14152 assert!(!generated_source.contains("faceOf(boxSolid"));
14153 let ast::Expr::CallExpressionKw(call) = cap_expr else {
14154 panic!("expected faceOf call");
14155 };
14156 assert_eq!(call.callee.name.name, "faceOf");
14157 let ast::Expr::Name(solid_name) = call.unlabeled.as_ref().unwrap() else {
14158 panic!("expected solid name");
14159 };
14160 assert_eq!(solid_name.name.name, "part");
14161 let ast::Expr::Name(face_name) = &call.arguments[0].arg else {
14162 panic!("expected face name");
14163 };
14164 assert_eq!(face_name.name.name, "END");
14165
14166 ctx.close().await;
14167 }
14168
14169 #[tokio::test(flavor = "multi_thread")]
14170 async fn test_sketch_on_plane_incremental() {
14171 let initial_source = "\
14172len = 2mm
14173cube = startSketchOn(XY)
14174 |> startProfile(at = [0, 0])
14175 |> line(end = [len, 0], tag = $side)
14176 |> line(end = [0, len])
14177 |> line(end = [-len, 0])
14178 |> line(end = [0, -len])
14179 |> close()
14180 |> extrude(length = len)
14181
14182plane = planeOf(cube, face = side)
14183";
14184
14185 let program = Program::parse(initial_source).unwrap().0.unwrap();
14186
14187 let mut frontend = FrontendState::new();
14188
14189 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14190 let mock_ctx = ExecutorContext::new_mock(None).await;
14191 let version = Version(0);
14192
14193 frontend.hack_set_program(&ctx, program).await.unwrap();
14194 let plane_object = frontend
14196 .scene_graph
14197 .objects
14198 .iter()
14199 .rev()
14200 .find(|object| matches!(&object.kind, ObjectKind::Plane(_)))
14201 .unwrap();
14202 let plane_id = plane_object.id;
14203
14204 let sketch_args = SketchCtor {
14205 on: Plane::Object(plane_id),
14206 };
14207 let (src_delta, scene_delta, sketch_id) = frontend
14208 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14209 .await
14210 .unwrap();
14211 assert_eq!(
14212 src_delta.text.as_str(),
14213 "\
14214len = 2mm
14215cube = startSketchOn(XY)
14216 |> startProfile(at = [0, 0])
14217 |> line(end = [len, 0], tag = $side)
14218 |> line(end = [0, len])
14219 |> line(end = [-len, 0])
14220 |> line(end = [0, -len])
14221 |> close()
14222 |> extrude(length = len)
14223
14224plane = planeOf(cube, face = side)
14225sketch001 = sketch(on = plane) {
14226}
14227"
14228 );
14229 assert_eq!(sketch_id, ObjectId(2));
14230 assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
14231 let sketch_object = &scene_delta.new_graph.objects[2];
14232 assert_eq!(sketch_object.id, ObjectId(2));
14233 assert_eq!(
14234 sketch_object.kind,
14235 ObjectKind::Sketch(Sketch {
14236 args: SketchCtor {
14237 on: Plane::Object(plane_id),
14238 },
14239 plane: plane_id,
14240 segments: vec![],
14241 constraints: vec![],
14242 })
14243 );
14244 assert_eq!(scene_delta.new_graph.objects.len(), 9);
14245
14246 let plane_object = scene_delta.new_graph.objects.get(plane_id.0).unwrap();
14247 assert_eq!(plane_object.id, plane_id);
14248 assert_eq!(plane_object.kind, ObjectKind::Plane(Plane::Object(plane_id)));
14249
14250 ctx.close().await;
14251 mock_ctx.close().await;
14252 }
14253
14254 #[tokio::test(flavor = "multi_thread")]
14255 async fn test_new_sketch_uses_unique_variable_name() {
14256 let initial_source = "\
14257sketch1 = sketch(on = XY) {
14258}
14259";
14260
14261 let program = Program::parse(initial_source).unwrap().0.unwrap();
14262
14263 let mut frontend = FrontendState::new();
14264 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14265 let version = Version(0);
14266
14267 frontend.hack_set_program(&ctx, program).await.unwrap();
14268
14269 let sketch_args = SketchCtor {
14270 on: Plane::Default(PlaneName::Yz),
14271 };
14272 let (src_delta, _, _) = frontend
14273 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14274 .await
14275 .unwrap();
14276
14277 assert_eq!(
14278 src_delta.text.as_str(),
14279 "\
14280sketch1 = sketch(on = XY) {
14281}
14282sketch001 = sketch(on = YZ) {
14283}
14284"
14285 );
14286
14287 ctx.close().await;
14288 }
14289
14290 #[tokio::test(flavor = "multi_thread")]
14291 async fn test_new_sketch_twice_using_same_plane() {
14292 let initial_source = "\
14293sketch1 = sketch(on = XY) {
14294}
14295";
14296
14297 let program = Program::parse(initial_source).unwrap().0.unwrap();
14298
14299 let mut frontend = FrontendState::new();
14300 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14301 let version = Version(0);
14302
14303 frontend.hack_set_program(&ctx, program).await.unwrap();
14304
14305 let sketch_args = SketchCtor {
14306 on: Plane::Default(PlaneName::Xy),
14307 };
14308 let (src_delta, _, _) = frontend
14309 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14310 .await
14311 .unwrap();
14312
14313 assert_eq!(
14314 src_delta.text.as_str(),
14315 "\
14316sketch1 = sketch(on = XY) {
14317}
14318sketch001 = sketch(on = XY) {
14319}
14320"
14321 );
14322
14323 ctx.close().await;
14324 }
14325
14326 #[tokio::test(flavor = "multi_thread")]
14327 async fn test_sketch_mode_reuses_cached_on_expression() {
14328 let initial_source = "\
14329width = 2mm
14330sketch(on = offsetPlane(XY, offset = width)) {
14331 line1 = line(start = [var 0, var 0], end = [var 1mm, var 0])
14332 distance([line1.start, line1.end]) == width
14333}
14334";
14335 let program = Program::parse(initial_source).unwrap().0.unwrap();
14336
14337 let mut frontend = FrontendState::new();
14338 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14339 let mock_ctx = ExecutorContext::new_mock(None).await;
14340 let version = Version(0);
14341 let project_id = ProjectId(0);
14342 let file_id = FileId(0);
14343
14344 frontend.hack_set_program(&ctx, program).await.unwrap();
14345 let initial_object_count = frontend.scene_graph.objects.len();
14346 let sketch_id = find_first_sketch_object(&frontend.scene_graph)
14347 .expect("Expected sketch object to exist")
14348 .id;
14349
14350 let scene_delta = frontend
14353 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
14354 .await
14355 .unwrap();
14356 assert_eq!(scene_delta.new_graph.objects.len(), initial_object_count);
14357
14358 let (_src_delta, scene_delta) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
14361 assert_eq!(scene_delta.new_graph.objects.len(), initial_object_count);
14362
14363 ctx.close().await;
14364 mock_ctx.close().await;
14365 }
14366
14367 #[tokio::test(flavor = "multi_thread")]
14368 async fn test_multiple_sketch_blocks() {
14369 let initial_source = "\
14370// Cube that requires the engine.
14371width = 2
14372sketch001 = startSketchOn(XY)
14373profile001 = startProfile(sketch001, at = [0, 0])
14374 |> yLine(length = width, tag = $seg1)
14375 |> xLine(length = width)
14376 |> yLine(length = -width)
14377 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
14378 |> close()
14379extrude001 = extrude(profile001, length = width)
14380
14381// Get a value that requires the engine.
14382x = segLen(seg1)
14383
14384// Triangle with side length 2*x.
14385sketch(on = XY) {
14386 line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
14387 line2 = line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
14388 coincident([line1.end, line2.start])
14389 line3 = line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
14390 coincident([line2.end, line3.start])
14391 coincident([line3.end, line1.start])
14392 equalLength([line3, line1])
14393 equalLength([line1, line2])
14394 distance([line1.start, line1.end]) == 2*x
14395}
14396
14397// Line segment with length x.
14398sketch2 = sketch(on = XY) {
14399 line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
14400 distance([line1.start, line1.end]) == x
14401}
14402";
14403
14404 let program = Program::parse(initial_source).unwrap().0.unwrap();
14405
14406 let mut frontend = FrontendState::new();
14407
14408 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14409 let mock_ctx = ExecutorContext::new_mock(None).await;
14410 let version = Version(0);
14411 let project_id = ProjectId(0);
14412 let file_id = FileId(0);
14413
14414 frontend.hack_set_program(&ctx, program).await.unwrap();
14415 let sketch_objects = frontend
14416 .scene_graph
14417 .objects
14418 .iter()
14419 .filter(|obj| matches!(obj.kind, ObjectKind::Sketch(_)))
14420 .collect::<Vec<_>>();
14421 let sketch1_id = sketch_objects.first().unwrap().id;
14422 let sketch2_id = sketch_objects.get(1).unwrap().id;
14423 let point1_id = ObjectId(sketch1_id.0 + 1);
14425 let point2_id = ObjectId(sketch2_id.0 + 1);
14427
14428 let scene_delta = frontend
14437 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch1_id)
14438 .await
14439 .unwrap();
14440 assert_eq!(
14441 scene_delta.new_graph.objects.len(),
14442 18,
14443 "{:#?}",
14444 scene_delta.new_graph.objects
14445 );
14446
14447 let point_ctor = PointCtor {
14449 position: Point2d {
14450 x: Expr::Var(Number {
14451 value: 1.0,
14452 units: NumericSuffix::Mm,
14453 }),
14454 y: Expr::Var(Number {
14455 value: 2.0,
14456 units: NumericSuffix::Mm,
14457 }),
14458 },
14459 };
14460 let segments = vec![ExistingSegmentCtor {
14461 id: point1_id,
14462 ctor: SegmentCtor::Point(point_ctor),
14463 }];
14464 let (src_delta, _) = frontend
14465 .edit_segments(&mock_ctx, version, sketch1_id, segments)
14466 .await
14467 .unwrap();
14468 assert_eq!(
14470 src_delta.text.as_str(),
14471 "\
14472// Cube that requires the engine.
14473width = 2
14474sketch001 = startSketchOn(XY)
14475profile001 = startProfile(sketch001, at = [0, 0])
14476 |> yLine(length = width, tag = $seg1)
14477 |> xLine(length = width)
14478 |> yLine(length = -width)
14479 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
14480 |> close()
14481extrude001 = extrude(profile001, length = width)
14482
14483// Get a value that requires the engine.
14484x = segLen(seg1)
14485
14486// Triangle with side length 2*x.
14487sketch(on = XY) {
14488 line1 = line(start = [var 1mm, var 2mm], end = [var 2.32mm, var -1.78mm])
14489 line2 = line(start = [var 2.32mm, var -1.78mm], end = [var -1.61mm, var -1.03mm])
14490 coincident([line1.end, line2.start])
14491 line3 = line(start = [var -1.61mm, var -1.03mm], end = [var 1mm, var 2mm])
14492 coincident([line2.end, line3.start])
14493 coincident([line3.end, line1.start])
14494 equalLength([line3, line1])
14495 equalLength([line1, line2])
14496 distance([line1.start, line1.end]) == 2 * x
14497}
14498
14499// Line segment with length x.
14500sketch2 = sketch(on = XY) {
14501 line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
14502 distance([line1.start, line1.end]) == x
14503}
14504"
14505 );
14506 let edited_sketch1_source = src_delta.text.clone();
14507
14508 let (src_delta, _) = frontend.execute_mock(&mock_ctx, version, sketch1_id).await.unwrap();
14510 assert_eq!(src_delta.text, edited_sketch1_source);
14511 let scene = frontend.exit_sketch(&ctx, version, sketch1_id).await.unwrap();
14519 assert_eq!(scene.objects.len(), 30, "{:#?}", scene.objects);
14520
14521 let scene_delta = frontend
14529 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch2_id)
14530 .await
14531 .unwrap();
14532 assert_eq!(
14533 scene_delta.new_graph.objects.len(),
14534 24,
14535 "{:#?}",
14536 scene_delta.new_graph.objects
14537 );
14538
14539 let point_ctor = PointCtor {
14541 position: Point2d {
14542 x: Expr::Var(Number {
14543 value: 3.0,
14544 units: NumericSuffix::Mm,
14545 }),
14546 y: Expr::Var(Number {
14547 value: 4.0,
14548 units: NumericSuffix::Mm,
14549 }),
14550 },
14551 };
14552 let segments = vec![ExistingSegmentCtor {
14553 id: point2_id,
14554 ctor: SegmentCtor::Point(point_ctor),
14555 }];
14556 let (src_delta, _) = frontend
14557 .edit_segments(&mock_ctx, version, sketch2_id, segments)
14558 .await
14559 .unwrap();
14560 assert_eq!(
14562 src_delta.text.as_str(),
14563 "\
14564// Cube that requires the engine.
14565width = 2
14566sketch001 = startSketchOn(XY)
14567profile001 = startProfile(sketch001, at = [0, 0])
14568 |> yLine(length = width, tag = $seg1)
14569 |> xLine(length = width)
14570 |> yLine(length = -width)
14571 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
14572 |> close()
14573extrude001 = extrude(profile001, length = width)
14574
14575// Get a value that requires the engine.
14576x = segLen(seg1)
14577
14578// Triangle with side length 2*x.
14579sketch(on = XY) {
14580 line1 = line(start = [var 1mm, var 2mm], end = [var 2.32mm, var -1.78mm])
14581 line2 = line(start = [var 2.32mm, var -1.78mm], end = [var -1.61mm, var -1.03mm])
14582 coincident([line1.end, line2.start])
14583 line3 = line(start = [var -1.61mm, var -1.03mm], end = [var 1mm, var 2mm])
14584 coincident([line2.end, line3.start])
14585 coincident([line3.end, line1.start])
14586 equalLength([line3, line1])
14587 equalLength([line1, line2])
14588 distance([line1.start, line1.end]) == 2 * x
14589}
14590
14591// Line segment with length x.
14592sketch2 = sketch(on = XY) {
14593 line1 = line(start = [var 3mm, var 4mm], end = [var 2.32mm, var 2.12mm])
14594 distance([line1.start, line1.end]) == x
14595}
14596"
14597 );
14598 let edited_sketch2_source = src_delta.text.clone();
14599
14600 let (src_delta, _) = frontend.execute_mock(&mock_ctx, version, sketch2_id).await.unwrap();
14602 assert_eq!(src_delta.text, edited_sketch2_source);
14603
14604 ctx.close().await;
14605 mock_ctx.close().await;
14606 }
14607
14608 #[tokio::test(flavor = "multi_thread")]
14609 async fn test_exit_sketch_without_changes_allows_entering_next_sketch() {
14610 clear_mem_cache().await;
14611
14612 let source = r#"sketch001 = sketch(on = XZ) {
14613 circle1 = circle(start = [var -1.96mm, var 2.77mm], center = [var -2.69mm, var 3.44mm])
14614}
14615sketch002 = sketch(on = XY) {
14616 line1 = line(start = [var 0mm, var 0mm], end = [var 4.68mm, var 0mm])
14617 line2 = line(start = [var 4.68mm, var 0mm], end = [var 4.68mm, var 2.96mm])
14618 line3 = line(start = [var 4.68mm, var 2.96mm], end = [var 0mm, var 2.96mm])
14619 line4 = line(start = [var 0mm, var 2.96mm], end = [var 0mm, var 0mm])
14620 coincident([line1.end, line2.start])
14621 coincident([line2.end, line3.start])
14622 coincident([line3.end, line4.start])
14623 coincident([line4.end, line1.start])
14624 parallel([line2, line4])
14625 parallel([line3, line1])
14626 perpendicular([line1, line2])
14627 horizontal(line3)
14628 coincident([line1.start, ORIGIN])
14629}
14630"#;
14631
14632 let program = Program::parse(source).unwrap().0.unwrap();
14633 let mut frontend = FrontendState::new();
14634 let ctx = ExecutorContext::new_with_engine(sync::Arc::new(EngineManager::new_mock()), Default::default());
14635 let mock_ctx = ExecutorContext::new_mock(None).await;
14636 let version = Version(0);
14637 let project_id = ProjectId(0);
14638 let file_id = FileId(0);
14639
14640 frontend.hack_set_program(&ctx, program).await.unwrap();
14641 let sketch_objects = frontend
14642 .scene_graph
14643 .objects
14644 .iter()
14645 .filter(|object| matches!(object.kind, ObjectKind::Sketch(_)))
14646 .collect::<Vec<_>>();
14647 assert_eq!(sketch_objects.len(), 2, "{:#?}", frontend.scene_graph.objects);
14648
14649 let sketch1_id = sketch_objects[0].id;
14650 let sketch2_id = sketch_objects[1].id;
14651
14652 frontend
14653 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch1_id)
14654 .await
14655 .unwrap();
14656 frontend.exit_sketch(&ctx, version, sketch1_id).await.unwrap();
14657
14658 let scene_delta = frontend
14659 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch2_id)
14660 .await
14661 .unwrap();
14662 assert_eq!(scene_delta.new_graph.sketch_mode, Some(sketch2_id));
14663
14664 clear_mem_cache().await;
14665 ctx.close().await;
14666 mock_ctx.close().await;
14667 }
14668
14669 #[tokio::test(flavor = "multi_thread")]
14674 async fn test_extra_newlines_after_settings_edit_sketch_add_point() {
14675 let initial_source = "@settings(defaultLengthUnit = mm)
14677
14678
14679
14680sketch001 = sketch(on = XY) {
14681 point(at = [1in, 2in])
14682}
14683";
14684
14685 let program = Program::parse(initial_source).unwrap().0.unwrap();
14686 let mut frontend = FrontendState::new();
14687
14688 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14689 let mock_ctx = ExecutorContext::new_mock(None).await;
14690 let version = Version(0);
14691 let project_id = ProjectId(0);
14692 let file_id = FileId(0);
14693
14694 frontend.hack_set_program(&ctx, program).await.unwrap();
14695 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14696 let sketch_id = sketch_object.id;
14697
14698 frontend
14700 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
14701 .await
14702 .unwrap();
14703
14704 let point_ctor = PointCtor {
14706 position: Point2d {
14707 x: Expr::Number(Number {
14708 value: 5.0,
14709 units: NumericSuffix::Mm,
14710 }),
14711 y: Expr::Number(Number {
14712 value: 6.0,
14713 units: NumericSuffix::Mm,
14714 }),
14715 },
14716 };
14717 let segment = SegmentCtor::Point(point_ctor);
14718 let (src_delta, scene_delta) = frontend
14719 .add_segment(&mock_ctx, version, sketch_id, segment, None)
14720 .await
14721 .unwrap();
14722 assert!(
14724 src_delta.text.contains("point(at = [5mm, 6mm])"),
14725 "Expected new point in source, got: {}",
14726 src_delta.text
14727 );
14728 assert!(!scene_delta.new_objects.is_empty());
14729
14730 ctx.close().await;
14731 mock_ctx.close().await;
14732 }
14733
14734 #[tokio::test(flavor = "multi_thread")]
14735 async fn test_ensure_control_point_spline_experimental_features_adds_allow_setting() {
14736 let initial_program = Program::parse("s = sketch(on = XY) {}\n").unwrap().0.unwrap();
14737
14738 let updated_program = ensure_control_point_spline_experimental_features(&initial_program).unwrap();
14739 let meta_settings = updated_program.meta_settings().unwrap().unwrap();
14740
14741 assert_eq!(meta_settings.experimental_features, WarningLevel::Allow);
14742 assert!(
14743 source_from_ast(&updated_program.ast).contains("@settings(experimentalFeatures = allow)"),
14744 "Expected experimental settings to be added to source"
14745 );
14746 }
14747
14748 #[tokio::test(flavor = "multi_thread")]
14749 async fn test_extra_newlines_after_settings_add_line_to_empty_sketch() {
14750 let initial_source = "@settings(defaultLengthUnit = mm)
14752
14753
14754
14755s = sketch(on = XY) {}
14756";
14757
14758 let program = Program::parse(initial_source).unwrap().0.unwrap();
14759 let mut frontend = FrontendState::new();
14760
14761 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14762 let mock_ctx = ExecutorContext::new_mock(None).await;
14763 let version = Version(0);
14764
14765 frontend.hack_set_program(&ctx, program).await.unwrap();
14766 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14767 let sketch_id = sketch_object.id;
14768
14769 let line_ctor = LineCtor {
14770 start: Point2d {
14771 x: Expr::Number(Number {
14772 value: 0.0,
14773 units: NumericSuffix::Mm,
14774 }),
14775 y: Expr::Number(Number {
14776 value: 0.0,
14777 units: NumericSuffix::Mm,
14778 }),
14779 },
14780 end: Point2d {
14781 x: Expr::Number(Number {
14782 value: 10.0,
14783 units: NumericSuffix::Mm,
14784 }),
14785 y: Expr::Number(Number {
14786 value: 10.0,
14787 units: NumericSuffix::Mm,
14788 }),
14789 },
14790 construction: None,
14791 };
14792 let segment = SegmentCtor::Line(line_ctor);
14793 let (src_delta, scene_delta) = frontend
14794 .add_segment(&mock_ctx, version, sketch_id, segment, None)
14795 .await
14796 .unwrap();
14797 assert!(
14798 src_delta.text.contains("line(start = [0mm, 0mm], end = [10mm, 10mm])"),
14799 "Expected line in source, got: {}",
14800 src_delta.text
14801 );
14802 assert_eq!(scene_delta.new_objects.len(), 3);
14804
14805 ctx.close().await;
14806 mock_ctx.close().await;
14807 }
14808
14809 #[tokio::test(flavor = "multi_thread")]
14810 async fn test_extra_newlines_between_operations_edit_line() {
14811 let initial_source = "@settings(defaultLengthUnit = mm)
14813
14814
14815sketch001 = sketch(on = XY) {
14816
14817 line1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm])
14818
14819}
14820";
14821
14822 let program = Program::parse(initial_source).unwrap().0.unwrap();
14823 let mut frontend = FrontendState::new();
14824
14825 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14826 let mock_ctx = ExecutorContext::new_mock(None).await;
14827 let version = Version(0);
14828 let project_id = ProjectId(0);
14829 let file_id = FileId(0);
14830
14831 frontend.hack_set_program(&ctx, program).await.unwrap();
14832 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14833 let sketch_id = sketch_object.id;
14834 let sketch = expect_sketch(sketch_object);
14835
14836 let line_id = sketch
14838 .segments
14839 .iter()
14840 .copied()
14841 .find(|seg_id| {
14842 matches!(
14843 &frontend.scene_graph.objects[seg_id.0].kind,
14844 ObjectKind::Segment {
14845 segment: Segment::Line(_)
14846 }
14847 )
14848 })
14849 .expect("Expected a line segment in sketch");
14850
14851 frontend
14853 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
14854 .await
14855 .unwrap();
14856
14857 let line_ctor = LineCtor {
14859 start: Point2d {
14860 x: Expr::Var(Number {
14861 value: 1.0,
14862 units: NumericSuffix::Mm,
14863 }),
14864 y: Expr::Var(Number {
14865 value: 2.0,
14866 units: NumericSuffix::Mm,
14867 }),
14868 },
14869 end: Point2d {
14870 x: Expr::Var(Number {
14871 value: 13.0,
14872 units: NumericSuffix::Mm,
14873 }),
14874 y: Expr::Var(Number {
14875 value: 14.0,
14876 units: NumericSuffix::Mm,
14877 }),
14878 },
14879 construction: None,
14880 };
14881 let segments = vec![ExistingSegmentCtor {
14882 id: line_id,
14883 ctor: SegmentCtor::Line(line_ctor),
14884 }];
14885 let (src_delta, _scene_delta) = frontend
14886 .edit_segments(&mock_ctx, version, sketch_id, segments)
14887 .await
14888 .unwrap();
14889 assert!(
14890 src_delta
14891 .text
14892 .contains("line(start = [var 1mm, var 2mm], end = [var 13mm, var 14mm])"),
14893 "Expected edited line in source, got: {}",
14894 src_delta.text
14895 );
14896
14897 ctx.close().await;
14898 mock_ctx.close().await;
14899 }
14900
14901 #[tokio::test(flavor = "multi_thread")]
14902 async fn test_extra_newlines_delete_segment() {
14903 let initial_source = "@settings(defaultLengthUnit = mm)
14905
14906
14907
14908sketch001 = sketch(on = XY) {
14909 circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
14910}
14911";
14912
14913 let program = Program::parse(initial_source).unwrap().0.unwrap();
14914 let mut frontend = FrontendState::new();
14915
14916 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14917 let mock_ctx = ExecutorContext::new_mock(None).await;
14918 let version = Version(0);
14919
14920 frontend.hack_set_program(&ctx, program).await.unwrap();
14921 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14922 let sketch_id = sketch_object.id;
14923 let sketch = expect_sketch(sketch_object);
14924
14925 assert_eq!(sketch.segments.len(), 3);
14927 let circle_id = sketch.segments[2];
14928
14929 let (src_delta, scene_delta) = frontend
14931 .delete_objects(&mock_ctx, version, sketch_id, vec![], vec![circle_id])
14932 .await
14933 .unwrap();
14934 assert!(
14935 src_delta.text.contains("sketch(on = XY) {"),
14936 "Expected sketch block in source, got: {}",
14937 src_delta.text
14938 );
14939 let new_sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
14940 let new_sketch = expect_sketch(new_sketch_object);
14941 assert_eq!(new_sketch.segments.len(), 0);
14942
14943 ctx.close().await;
14944 mock_ctx.close().await;
14945 }
14946
14947 #[tokio::test(flavor = "multi_thread")]
14948 async fn test_unformatted_source_add_arc() {
14949 let initial_source = "@settings(defaultLengthUnit = mm)
14951
14952
14953
14954
14955sketch001 = sketch(on = XY) {
14956}
14957";
14958
14959 let program = Program::parse(initial_source).unwrap().0.unwrap();
14960 let mut frontend = FrontendState::new();
14961
14962 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14963 let mock_ctx = ExecutorContext::new_mock(None).await;
14964 let version = Version(0);
14965
14966 frontend.hack_set_program(&ctx, program).await.unwrap();
14967 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14968 let sketch_id = sketch_object.id;
14969
14970 let arc_ctor = ArcCtor {
14971 start: Point2d {
14972 x: Expr::Var(Number {
14973 value: 5.0,
14974 units: NumericSuffix::Mm,
14975 }),
14976 y: Expr::Var(Number {
14977 value: 0.0,
14978 units: NumericSuffix::Mm,
14979 }),
14980 },
14981 end: Point2d {
14982 x: Expr::Var(Number {
14983 value: 0.0,
14984 units: NumericSuffix::Mm,
14985 }),
14986 y: Expr::Var(Number {
14987 value: 5.0,
14988 units: NumericSuffix::Mm,
14989 }),
14990 },
14991 center: Point2d {
14992 x: Expr::Var(Number {
14993 value: 0.0,
14994 units: NumericSuffix::Mm,
14995 }),
14996 y: Expr::Var(Number {
14997 value: 0.0,
14998 units: NumericSuffix::Mm,
14999 }),
15000 },
15001 construction: None,
15002 };
15003 let segment = SegmentCtor::Arc(arc_ctor);
15004 let (src_delta, scene_delta) = frontend
15005 .add_segment(&mock_ctx, version, sketch_id, segment, None)
15006 .await
15007 .unwrap();
15008 assert!(
15009 src_delta
15010 .text
15011 .contains("arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])"),
15012 "Expected arc in source, got: {}",
15013 src_delta.text
15014 );
15015 assert!(!scene_delta.new_objects.is_empty());
15016
15017 ctx.close().await;
15018 mock_ctx.close().await;
15019 }
15020
15021 #[tokio::test(flavor = "multi_thread")]
15022 async fn test_extra_newlines_add_circle() {
15023 let initial_source = "@settings(defaultLengthUnit = mm)
15025
15026
15027
15028sketch001 = sketch(on = XY) {
15029}
15030";
15031
15032 let program = Program::parse(initial_source).unwrap().0.unwrap();
15033 let mut frontend = FrontendState::new();
15034
15035 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
15036 let mock_ctx = ExecutorContext::new_mock(None).await;
15037 let version = Version(0);
15038
15039 frontend.hack_set_program(&ctx, program).await.unwrap();
15040 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15041 let sketch_id = sketch_object.id;
15042
15043 let circle_ctor = CircleCtor {
15044 start: Point2d {
15045 x: Expr::Var(Number {
15046 value: 5.0,
15047 units: NumericSuffix::Mm,
15048 }),
15049 y: Expr::Var(Number {
15050 value: 0.0,
15051 units: NumericSuffix::Mm,
15052 }),
15053 },
15054 center: Point2d {
15055 x: Expr::Var(Number {
15056 value: 0.0,
15057 units: NumericSuffix::Mm,
15058 }),
15059 y: Expr::Var(Number {
15060 value: 0.0,
15061 units: NumericSuffix::Mm,
15062 }),
15063 },
15064 construction: None,
15065 };
15066 let segment = SegmentCtor::Circle(circle_ctor);
15067 let (src_delta, scene_delta) = frontend
15068 .add_segment(&mock_ctx, version, sketch_id, segment, None)
15069 .await
15070 .unwrap();
15071 assert!(
15072 src_delta
15073 .text
15074 .contains("circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])"),
15075 "Expected circle in source, got: {}",
15076 src_delta.text
15077 );
15078 assert!(!scene_delta.new_objects.is_empty());
15079
15080 ctx.close().await;
15081 mock_ctx.close().await;
15082 }
15083
15084 #[tokio::test(flavor = "multi_thread")]
15085 async fn test_extra_newlines_add_constraint() {
15086 let initial_source = "@settings(defaultLengthUnit = mm)
15088
15089
15090
15091sketch001 = sketch(on = XY) {
15092 line1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm])
15093 line2 = line(start = [var 10mm, var 10mm], end = [var 20mm, var 0mm])
15094}
15095";
15096
15097 let program = Program::parse(initial_source).unwrap().0.unwrap();
15098 let mut frontend = FrontendState::new();
15099
15100 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
15101 let mock_ctx = ExecutorContext::new_mock(None).await;
15102 let version = Version(0);
15103 let project_id = ProjectId(0);
15104 let file_id = FileId(0);
15105
15106 frontend.hack_set_program(&ctx, program).await.unwrap();
15107 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15108 let sketch_id = sketch_object.id;
15109 let sketch = expect_sketch(sketch_object);
15110
15111 let line_ids: Vec<ObjectId> = sketch
15113 .segments
15114 .iter()
15115 .copied()
15116 .filter(|seg_id| {
15117 matches!(
15118 &frontend.scene_graph.objects[seg_id.0].kind,
15119 ObjectKind::Segment {
15120 segment: Segment::Line(_)
15121 }
15122 )
15123 })
15124 .collect();
15125 assert_eq!(line_ids.len(), 2, "Expected two line segments");
15126
15127 let line1 = &frontend.scene_graph.objects[line_ids[0].0];
15128 let ObjectKind::Segment {
15129 segment: Segment::Line(line1_data),
15130 } = &line1.kind
15131 else {
15132 panic!("Expected line");
15133 };
15134 let line2 = &frontend.scene_graph.objects[line_ids[1].0];
15135 let ObjectKind::Segment {
15136 segment: Segment::Line(line2_data),
15137 } = &line2.kind
15138 else {
15139 panic!("Expected line");
15140 };
15141
15142 let constraint = Constraint::Coincident(Coincident {
15144 segments: vec![line1_data.end.into(), line2_data.start.into()],
15145 });
15146
15147 frontend
15149 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
15150 .await
15151 .unwrap();
15152 let (src_delta, _scene_delta) = frontend
15153 .add_constraint(&mock_ctx, version, sketch_id, constraint)
15154 .await
15155 .unwrap();
15156 assert!(
15157 src_delta.text.contains("coincident("),
15158 "Expected coincident constraint in source, got: {}",
15159 src_delta.text
15160 );
15161
15162 ctx.close().await;
15163 mock_ctx.close().await;
15164 }
15165
15166 #[tokio::test(flavor = "multi_thread")]
15167 async fn test_extra_newlines_add_line_then_edit_line() {
15168 let initial_source = "@settings(defaultLengthUnit = mm)
15170
15171
15172
15173sketch001 = sketch(on = XY) {
15174}
15175";
15176
15177 let program = Program::parse(initial_source).unwrap().0.unwrap();
15178 let mut frontend = FrontendState::new();
15179
15180 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
15181 let mock_ctx = ExecutorContext::new_mock(None).await;
15182 let version = Version(0);
15183
15184 frontend.hack_set_program(&ctx, program).await.unwrap();
15185 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15186 let sketch_id = sketch_object.id;
15187
15188 let line_ctor = LineCtor {
15190 start: Point2d {
15191 x: Expr::Number(Number {
15192 value: 0.0,
15193 units: NumericSuffix::Mm,
15194 }),
15195 y: Expr::Number(Number {
15196 value: 0.0,
15197 units: NumericSuffix::Mm,
15198 }),
15199 },
15200 end: Point2d {
15201 x: Expr::Number(Number {
15202 value: 10.0,
15203 units: NumericSuffix::Mm,
15204 }),
15205 y: Expr::Number(Number {
15206 value: 10.0,
15207 units: NumericSuffix::Mm,
15208 }),
15209 },
15210 construction: None,
15211 };
15212 let segment = SegmentCtor::Line(line_ctor);
15213 let (src_delta, scene_delta) = frontend
15214 .add_segment(&mock_ctx, version, sketch_id, segment, None)
15215 .await
15216 .unwrap();
15217 assert!(
15218 src_delta.text.contains("line(start = [0mm, 0mm], end = [10mm, 10mm])"),
15219 "Expected line in source after add, got: {}",
15220 src_delta.text
15221 );
15222 let line_id = *scene_delta.new_objects.last().unwrap();
15224
15225 let line_ctor = LineCtor {
15227 start: Point2d {
15228 x: Expr::Number(Number {
15229 value: 1.0,
15230 units: NumericSuffix::Mm,
15231 }),
15232 y: Expr::Number(Number {
15233 value: 2.0,
15234 units: NumericSuffix::Mm,
15235 }),
15236 },
15237 end: Point2d {
15238 x: Expr::Number(Number {
15239 value: 13.0,
15240 units: NumericSuffix::Mm,
15241 }),
15242 y: Expr::Number(Number {
15243 value: 14.0,
15244 units: NumericSuffix::Mm,
15245 }),
15246 },
15247 construction: None,
15248 };
15249 let segments = vec![ExistingSegmentCtor {
15250 id: line_id,
15251 ctor: SegmentCtor::Line(line_ctor),
15252 }];
15253 let (src_delta, scene_delta) = frontend
15254 .edit_segments(&mock_ctx, version, sketch_id, segments)
15255 .await
15256 .unwrap();
15257 assert!(
15258 src_delta.text.contains("line(start = [1mm, 2mm], end = [13mm, 14mm])"),
15259 "Expected edited line in source, got: {}",
15260 src_delta.text
15261 );
15262 assert_eq!(scene_delta.new_objects, vec![]);
15263
15264 ctx.close().await;
15265 mock_ctx.close().await;
15266 }
15267}