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::Expr;
53use crate::frontend::api::FileId;
54use crate::frontend::api::Number;
55use crate::frontend::api::ObjectId;
56use crate::frontend::api::ObjectKind;
57use crate::frontend::api::Plane;
58use crate::frontend::api::ProjectId;
59use crate::frontend::api::RestoreSketchCheckpointOutcome;
60use crate::frontend::api::SceneGraph;
61use crate::frontend::api::SceneGraphDelta;
62use crate::frontend::api::SketchCheckpointId;
63use crate::frontend::api::SourceDelta;
64use crate::frontend::api::SourceRef;
65use crate::frontend::api::Version;
66use crate::frontend::modify::find_defined_names;
67use crate::frontend::modify::next_free_name;
68use crate::frontend::modify::next_free_name_with_padding;
69use crate::frontend::sketch::Coincident;
70use crate::frontend::sketch::Constraint;
71use crate::frontend::sketch::ConstraintSegment;
72use crate::frontend::sketch::Diameter;
73use crate::frontend::sketch::ExistingSegmentCtor;
74use crate::frontend::sketch::Horizontal;
75use crate::frontend::sketch::LineCtor;
76use crate::frontend::sketch::Point2d;
77use crate::frontend::sketch::Radius;
78use crate::frontend::sketch::Segment;
79use crate::frontend::sketch::SegmentCtor;
80use crate::frontend::sketch::SketchApi;
81use crate::frontend::sketch::SketchCtor;
82use crate::frontend::sketch::Vertical;
83use crate::frontend::traverse::MutateBodyItem;
84use crate::frontend::traverse::TraversalReturn;
85use crate::frontend::traverse::Visitor;
86use crate::frontend::traverse::dfs_mut;
87use crate::id::IncIdGenerator;
88use crate::parsing::ast::types as ast;
89use crate::parsing::ast::types::NodePathExt;
90use crate::pretty::NumericSuffix;
91use crate::std::constraints::LinesAtAngleKind;
92use crate::walk::NodeMut;
93use crate::walk::Visitable;
94
95pub(crate) mod api;
96pub(crate) mod modify;
97pub(crate) mod sketch;
98
99pub const MAX_SKETCH_CHECKPOINTS: usize = 100;
100
101#[derive(Debug, Clone)]
102struct SketchCheckpoint {
103 id: SketchCheckpointId,
104 source: SourceDelta,
105 program: Program,
106 scene_graph: SceneGraph,
107 exec_outcome: ExecOutcome,
108 point_freedom_cache: HashMap<ObjectId, Freedom>,
109 mock_memory: Option<SketchModeState>,
110}
111mod traverse;
112pub(crate) mod trim;
113
114struct ArcSizeConstraintParams {
115 points: Vec<ObjectId>,
116 function_name: &'static str,
117 value: f64,
118 units: NumericSuffix,
119 label_position: Option<Point2d<Number>>,
120 constraint_type_name: &'static str,
121}
122
123const POINT_FN: &str = "point";
124const POINT_AT_PARAM: &str = "at";
125const LINE_FN: &str = "line";
126const LINE_VARIABLE: &str = "line";
127const LINE_START_PARAM: &str = "start";
128const LINE_END_PARAM: &str = "end";
129const ARC_FN: &str = "arc";
130const ARC_VARIABLE: &str = "arc";
131const ARC_START_PARAM: &str = "start";
132const ARC_END_PARAM: &str = "end";
133const ARC_CENTER_PARAM: &str = "center";
134const CIRCLE_FN: &str = "circle";
135const CIRCLE_VARIABLE: &str = "circle";
136const CIRCLE_START_PARAM: &str = "start";
137const CIRCLE_CENTER_PARAM: &str = "center";
138const CONTROL_POINT_SPLINE_FN: &str = "controlPointSpline";
139const CONTROL_POINT_SPLINE_POINTS_PARAM: &str = "points";
140const LABEL_POSITION_PARAM: &str = "labelPosition";
141
142const COINCIDENT_FN: &str = "coincident";
143const DIAMETER_FN: &str = "diameter";
144const DISTANCE_FN: &str = "distance";
145const FIXED_FN: &str = "fixed";
146const ANGLE_FN: &str = "angle";
147const HORIZONTAL_DISTANCE_FN: &str = "horizontalDistance";
148const VERTICAL_DISTANCE_FN: &str = "verticalDistance";
149const EQUAL_LENGTH_FN: &str = "equalLength";
150const EQUAL_RADIUS_FN: &str = "equalRadius";
151const HORIZONTAL_FN: &str = "horizontal";
152const MIDPOINT_FN: &str = "midpoint";
153const MIDPOINT_POINT_PARAM: &str = "point";
154const RADIUS_FN: &str = "radius";
155const SYMMETRIC_FN: &str = "symmetric";
156const SYMMETRIC_AXIS_PARAM: &str = "axis";
157const TANGENT_FN: &str = "tangent";
158const VERTICAL_FN: &str = "vertical";
159
160const LINE_PROPERTY_START: &str = "start";
161const LINE_PROPERTY_END: &str = "end";
162
163const ARC_PROPERTY_START: &str = "start";
164const ARC_PROPERTY_END: &str = "end";
165const ARC_PROPERTY_CENTER: &str = "center";
166const CIRCLE_PROPERTY_START: &str = "start";
167const CIRCLE_PROPERTY_CENTER: &str = "center";
168const CONTROL_POINT_SPLINE_PROPERTY_CONTROLS: &str = "controls";
169const CONTROL_POINT_SPLINE_PROPERTY_EDGES: &str = "edges";
170
171const CONSTRUCTION_PARAM: &str = "construction";
172
173#[derive(Debug, Clone, Copy)]
174enum EditDeleteKind {
175 Edit,
176 DeleteNonSketch,
177}
178
179struct ExecuteAfterEditOptions {
181 segment_ids_edited: AhashIndexSet<ObjectId>,
182 edit_kind: EditDeleteKind,
183 commit_solved_initial_guesses: bool,
184}
185
186impl EditDeleteKind {
187 fn is_delete(&self) -> bool {
189 match self {
190 EditDeleteKind::Edit => false,
191 EditDeleteKind::DeleteNonSketch => true,
192 }
193 }
194
195 fn to_change_kind(self) -> ChangeKind {
196 match self {
197 EditDeleteKind::Edit => ChangeKind::Edit,
198 EditDeleteKind::DeleteNonSketch => ChangeKind::Delete,
199 }
200 }
201}
202
203#[derive(Debug, Clone, Copy)]
204enum ChangeKind {
205 Add,
206 Edit,
207 Delete,
208 None,
209}
210
211#[derive(Debug, Clone, Serialize, ts_rs::TS)]
212#[ts(export, export_to = "FrontendApi.ts")]
213#[serde(tag = "type")]
214pub enum SetProgramOutcome {
215 #[serde(rename_all = "camelCase")]
216 Success {
217 scene_graph: Box<SceneGraph>,
218 exec_outcome: Box<ExecOutcome>,
219 checkpoint_id: Option<SketchCheckpointId>,
220 },
221 #[serde(rename_all = "camelCase")]
222 ExecFailure { error: Box<KclErrorWithOutputs> },
223}
224
225pub struct EditSegmentsOptions {
227 pub anchor_segment_ids: Option<Vec<ObjectId>>,
233 pub drag_anchors: Vec<SegmentDragAnchor>,
236 pub commit_solved_initial_guesses: bool,
238}
239
240pub struct EditDistanceConstraintLabelPositionOptions {
242 pub anchor_segment_ids: Vec<ObjectId>,
244 pub commit_solved_initial_guesses: bool,
246}
247
248#[derive(Debug, Clone)]
249pub struct FrontendState {
250 program: Program,
251 scene_graph: SceneGraph,
252 point_freedom_cache: HashMap<ObjectId, Freedom>,
255 next_drag_anchor_segment_ids: Option<AhashIndexSet<ObjectId>>,
258 next_segment_drag_anchors: Option<Vec<SegmentDragAnchor>>,
261 next_edit_commits_solver_solutions: Option<bool>,
265 sketch_checkpoints: VecDeque<SketchCheckpoint>,
266 sketch_checkpoint_id_gen: IncIdGenerator<u64>,
267}
268
269impl Default for FrontendState {
270 fn default() -> Self {
271 Self::new()
272 }
273}
274
275impl FrontendState {
276 pub fn new() -> Self {
277 Self {
278 program: Program::empty(),
279 scene_graph: SceneGraph {
280 project: ProjectId(0),
281 file: FileId(0),
282 version: Version(0),
283 objects: Default::default(),
284 settings: Default::default(),
285 sketch_mode: Default::default(),
286 },
287 point_freedom_cache: HashMap::new(),
288 next_drag_anchor_segment_ids: None,
289 next_segment_drag_anchors: None,
290 next_edit_commits_solver_solutions: None,
291 sketch_checkpoints: VecDeque::new(),
292 sketch_checkpoint_id_gen: IncIdGenerator::new(1),
293 }
294 }
295
296 pub fn scene_graph(&self) -> &SceneGraph {
298 &self.scene_graph
299 }
300
301 pub fn default_length_unit(&self) -> UnitLength {
302 self.program
303 .meta_settings()
304 .ok()
305 .flatten()
306 .map(|settings| settings.default_length_units)
307 .unwrap_or(UnitLength::Millimeters)
308 }
309
310 pub async fn create_sketch_checkpoint(&mut self, exec_outcome: ExecOutcome) -> api::Result<SketchCheckpointId> {
311 let checkpoint_id = SketchCheckpointId::new(self.sketch_checkpoint_id_gen.next_id());
312
313 let checkpoint = SketchCheckpoint {
314 id: checkpoint_id,
315 source: SourceDelta {
316 text: source_from_ast(&self.program.ast),
317 },
318 program: self.program.clone(),
319 scene_graph: self.scene_graph.clone(),
320 exec_outcome,
321 point_freedom_cache: self.point_freedom_cache.clone(),
322 mock_memory: read_old_memory().await,
323 };
324
325 self.sketch_checkpoints.push_back(checkpoint);
326 while self.sketch_checkpoints.len() > MAX_SKETCH_CHECKPOINTS {
327 self.sketch_checkpoints.pop_front();
328 }
329
330 Ok(checkpoint_id)
331 }
332
333 pub async fn edit_segments_with_options(
341 &mut self,
342 ctx: &ExecutorContext,
343 version: Version,
344 sketch: ObjectId,
345 segments: Vec<ExistingSegmentCtor>,
346 options: EditSegmentsOptions,
347 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
348 let previous_anchor_ids = options.anchor_segment_ids.map(|anchor_ids| {
349 self.next_drag_anchor_segment_ids
350 .replace(anchor_ids.into_iter().collect())
351 });
352 let previous_drag_anchors = self.next_segment_drag_anchors.replace(options.drag_anchors);
353 let previous_commit_mode = self
354 .next_edit_commits_solver_solutions
355 .replace(options.commit_solved_initial_guesses);
356 let result = SketchApi::edit_segments(self, ctx, version, sketch, segments).await;
357 if let Some(previous_anchor_ids) = previous_anchor_ids {
358 self.next_drag_anchor_segment_ids = previous_anchor_ids;
359 }
360 self.next_segment_drag_anchors = previous_drag_anchors;
361 self.next_edit_commits_solver_solutions = previous_commit_mode;
362 result
363 }
364
365 pub async fn edit_distance_constraint_label_position_with_options(
371 &mut self,
372 ctx: &ExecutorContext,
373 version: Version,
374 sketch: ObjectId,
375 constraint_id: ObjectId,
376 label_position: Point2d<Number>,
377 options: EditDistanceConstraintLabelPositionOptions,
378 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
379 let previous_commit_mode = self
380 .next_edit_commits_solver_solutions
381 .replace(options.commit_solved_initial_guesses);
382 let result = SketchApi::edit_distance_constraint_label_position(
383 self,
384 ctx,
385 version,
386 sketch,
387 constraint_id,
388 label_position,
389 options.anchor_segment_ids,
390 )
391 .await;
392 self.next_edit_commits_solver_solutions = previous_commit_mode;
393 result
394 }
395
396 pub async fn restore_sketch_checkpoint(
397 &mut self,
398 checkpoint_id: SketchCheckpointId,
399 ) -> api::Result<RestoreSketchCheckpointOutcome> {
400 let checkpoint = self
401 .sketch_checkpoints
402 .iter()
403 .find(|checkpoint| checkpoint.id == checkpoint_id)
404 .cloned()
405 .ok_or_else(|| Error {
406 msg: format!("Sketch checkpoint not found: {checkpoint_id:?}"),
407 })?;
408
409 self.program = checkpoint.program;
410 self.scene_graph = checkpoint.scene_graph.clone();
411 self.point_freedom_cache = checkpoint.point_freedom_cache;
412 self.next_drag_anchor_segment_ids = None;
413 self.next_segment_drag_anchors = None;
414 self.next_edit_commits_solver_solutions = None;
415
416 if let Some(mock_memory) = checkpoint.mock_memory {
417 write_old_memory(mock_memory).await;
418 } else {
419 clear_mem_cache().await;
420 }
421
422 Ok(RestoreSketchCheckpointOutcome {
423 source_delta: checkpoint.source,
424 scene_graph_delta: SceneGraphDelta {
425 new_graph: self.scene_graph_for_ui(),
426 new_objects: Vec::new(),
427 invalidates_ids: true,
428 exec_outcome: checkpoint.exec_outcome,
429 },
430 })
431 }
432
433 pub fn clear_sketch_checkpoints(&mut self) {
434 self.sketch_checkpoints.clear();
435 }
436 fn scene_graph_for_ui(&self) -> SceneGraph {
437 let has_control_point_splines = self.scene_graph.objects.iter().any(|object| {
438 matches!(
439 object.kind,
440 ObjectKind::Segment {
441 segment: Segment::ControlPointSpline(_)
442 }
443 )
444 });
445
446 if !has_control_point_splines {
447 return self.scene_graph.clone();
448 }
449
450 let hidden_constraint_ids = self
451 .scene_graph
452 .objects
453 .iter()
454 .filter_map(|object| match &object.kind {
455 ObjectKind::Constraint {
456 constraint: Constraint::Coincident(coincident),
457 } if coincident_is_internal_to_same_control_point_spline(coincident, &self.scene_graph) => {
458 Some(object.id)
459 }
460 _ => None,
461 })
462 .collect::<HashSet<_>>();
463
464 if hidden_constraint_ids.is_empty() {
465 return self.scene_graph.clone();
466 }
467
468 let mut scene_graph = self.scene_graph.clone();
469 for object in &mut scene_graph.objects {
470 match &mut object.kind {
471 ObjectKind::Constraint { .. } if hidden_constraint_ids.contains(&object.id) => {
472 object.kind = ObjectKind::Nil;
473 }
474 ObjectKind::Sketch(sketch) => {
475 sketch
476 .constraints
477 .retain(|constraint_id| !hidden_constraint_ids.contains(constraint_id));
478 }
479 _ => {}
480 }
481 }
482
483 scene_graph
484 }
485}
486
487fn coincident_is_internal_to_same_control_point_spline(coincident: &Coincident, scene_graph: &SceneGraph) -> bool {
488 let mut first_owner_id = None;
489 for segment_id in coincident.segment_ids() {
490 let Some(owner_id) = owning_control_point_spline_id(segment_id, scene_graph) else {
491 return false;
492 };
493
494 match first_owner_id {
495 Some(first_owner_id) if first_owner_id != owner_id => return false,
496 Some(_) => {}
497 None => first_owner_id = Some(owner_id),
498 }
499 }
500
501 first_owner_id.is_some()
502}
503
504fn owning_control_point_spline_id(segment_id: ObjectId, scene_graph: &SceneGraph) -> Option<ObjectId> {
505 let object = scene_graph.objects.get(segment_id.0)?;
506 let ObjectKind::Segment { segment } = &object.kind else {
507 return None;
508 };
509
510 match segment {
511 Segment::ControlPointSpline(_) => Some(segment_id),
512 Segment::Point(point) => point
513 .owner
514 .filter(|owner_id| matches_control_point_spline_owner(*owner_id, scene_graph)),
515 Segment::Line(line) => line
516 .owner
517 .filter(|owner_id| matches_control_point_spline_owner(*owner_id, scene_graph)),
518 _ => None,
519 }
520}
521
522fn matches_control_point_spline_owner(owner_id: ObjectId, scene_graph: &SceneGraph) -> bool {
523 matches!(
524 scene_graph.objects.get(owner_id.0).map(|object| &object.kind),
525 Some(ObjectKind::Segment {
526 segment: Segment::ControlPointSpline(_)
527 })
528 )
529}
530
531fn ensure_control_point_spline_experimental_features(program: &Program) -> Result<Program, KclError> {
532 let experimental_features_allowed = program
533 .meta_settings()
534 .ok()
535 .flatten()
536 .map(|settings| settings.experimental_features == WarningLevel::Allow)
537 .unwrap_or(false);
538 if experimental_features_allowed {
539 return Ok(program.clone());
540 }
541
542 program.change_experimental_features(Some(WarningLevel::Allow))
543}
544
545impl SketchApi for FrontendState {
546 async fn execute_mock(
547 &mut self,
548 ctx: &ExecutorContext,
549 _version: Version,
550 sketch: ObjectId,
551 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
552 let sketch_block_ref =
553 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
554
555 let mut truncated_program = self.program.clone();
556 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::None)
557 .map_err(KclErrorWithOutputs::no_outputs)?;
558
559 let outcome = ctx
561 .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch))
562 .await?;
563 let new_source = source_from_ast(&self.program.ast);
564 let src_delta = SourceDelta { text: new_source };
565 let outcome = self.update_state_after_exec(outcome, true);
567 let scene_graph_delta = SceneGraphDelta {
568 new_graph: self.scene_graph.clone(),
569 new_objects: Default::default(),
570 invalidates_ids: false,
571 exec_outcome: outcome,
572 };
573 Ok((src_delta, scene_graph_delta))
574 }
575
576 async fn new_sketch(
577 &mut self,
578 ctx: &ExecutorContext,
579 _project: ProjectId,
580 _file: FileId,
581 _version: Version,
582 args: SketchCtor,
583 ) -> ExecResult<(SourceDelta, SceneGraphDelta, ObjectId)> {
584 let mut new_ast = self.program.ast.clone();
587 let mut plane_ast =
589 sketch_on_ast_expr(&mut new_ast, &self.scene_graph, &args.on).map_err(KclErrorWithOutputs::no_outputs)?;
590 let mut defined_names = find_defined_names(&new_ast);
591 let is_face_of_expr = matches!(
592 &plane_ast,
593 ast::Expr::CallExpressionKw(call) if call.callee.name.name == "faceOf"
594 );
595 if is_face_of_expr {
596 let face_name = next_free_name_with_padding("face", &defined_names)
597 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
598 let face_decl = ast::VariableDeclaration::new(
599 ast::VariableDeclarator::new(&face_name, plane_ast),
600 ast::ItemVisibility::Default,
601 ast::VariableKind::Const,
602 );
603 new_ast
604 .body
605 .push(ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(
606 face_decl,
607 ))));
608 defined_names.insert(face_name.clone());
609 plane_ast = ast::Expr::Name(Box::new(ast::Name::new(&face_name)));
610 }
611 let sketch_ast = ast::SketchBlock {
612 arguments: vec![ast::LabeledArg {
613 label: Some(ast::Identifier::new(SKETCH_BLOCK_PARAM_ON)),
614 arg: plane_ast,
615 }],
616 body: Default::default(),
617 is_being_edited: false,
618 non_code_meta: Default::default(),
619 digest: None,
620 };
621 let sketch_name = next_free_name_with_padding("sketch", &defined_names)
624 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
625 let sketch_decl = ast::VariableDeclaration::new(
626 ast::VariableDeclarator::new(
627 &sketch_name,
628 ast::Expr::SketchBlock(Box::new(ast::Node::no_src(sketch_ast))),
629 ),
630 ast::ItemVisibility::Default,
631 ast::VariableKind::Const,
632 );
633 new_ast
634 .body
635 .push(ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(
636 sketch_decl,
637 ))));
638 let new_source = source_from_ast(&new_ast);
640 let (new_program, errors) = Program::parse(&new_source)
642 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
643 if !errors.is_empty() {
644 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
645 "Error parsing KCL source after adding sketch: {errors:?}"
646 ))));
647 }
648 let Some(new_program) = new_program else {
649 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
650 "No AST produced after adding sketch".to_owned(),
651 )));
652 };
653
654 self.program = new_program.clone();
656
657 let outcome = ctx.run_with_caching(new_program.clone()).await?;
660 let freedom_analysis_ran = true;
661
662 let outcome = self.update_state_after_exec(outcome, freedom_analysis_ran);
663
664 let Some(sketch_id) = self
665 .scene_graph
666 .objects
667 .iter()
668 .filter_map(|object| match object.kind {
669 ObjectKind::Sketch(_) => Some(object.id),
670 _ => None,
671 })
672 .max_by_key(|id| id.0)
673 else {
674 return Err(KclErrorWithOutputs::from_error_outcome(
675 KclError::refactor("No objects in scene graph after adding sketch".to_owned()),
676 outcome,
677 ));
678 };
679 self.scene_graph.sketch_mode = Some(sketch_id);
681
682 let src_delta = SourceDelta { text: new_source };
683 let scene_graph_delta = SceneGraphDelta {
684 new_graph: self.scene_graph_for_ui(),
685 invalidates_ids: false,
686 new_objects: vec![sketch_id],
687 exec_outcome: outcome,
688 };
689 Ok((src_delta, scene_graph_delta, sketch_id))
690 }
691
692 async fn edit_sketch(
693 &mut self,
694 ctx: &ExecutorContext,
695 _project: ProjectId,
696 _file: FileId,
697 _version: Version,
698 sketch: ObjectId,
699 ) -> ExecResult<SceneGraphDelta> {
700 let sketch_object = self.scene_graph.objects.get(sketch.0).ok_or_else(|| {
704 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
705 })?;
706 let ObjectKind::Sketch(_) = &sketch_object.kind else {
707 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
708 "Object is not a sketch, it is {}",
709 sketch_object.kind.human_friendly_kind_with_article()
710 ))));
711 };
712 let sketch_block_ref = expect_single_node_ref(sketch_object).map_err(KclErrorWithOutputs::no_outputs)?;
713
714 self.scene_graph.sketch_mode = Some(sketch);
716
717 let mut truncated_program = self.program.clone();
719 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::None)
720 .map_err(KclErrorWithOutputs::no_outputs)?;
721
722 let outcome = ctx
725 .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch))
726 .await?;
727
728 let outcome = self.update_state_after_exec(outcome, true);
730 let scene_graph_delta = SceneGraphDelta {
731 new_graph: self.scene_graph_for_ui(),
732 invalidates_ids: false,
733 new_objects: Vec::new(),
734 exec_outcome: outcome,
735 };
736 Ok(scene_graph_delta)
737 }
738
739 async fn exit_sketch(
740 &mut self,
741 ctx: &ExecutorContext,
742 _version: Version,
743 sketch: ObjectId,
744 ) -> ExecResult<SceneGraph> {
745 #[cfg(not(target_arch = "wasm32"))]
747 let _ = sketch;
748 #[cfg(target_arch = "wasm32")]
749 if self.scene_graph.sketch_mode != Some(sketch) {
750 web_sys::console::warn_1(
751 &format!(
752 "WARNING: exit_sketch: current state's sketch mode ID doesn't match the given sketch ID; state={:#?}, given={sketch:?}",
753 &self.scene_graph.sketch_mode
754 )
755 .into(),
756 );
757 }
758 self.scene_graph.sketch_mode = None;
759
760 let outcome = ctx.run_with_caching(self.program.clone()).await?;
762
763 self.update_state_after_exec(outcome, false);
765
766 Ok(self.scene_graph_for_ui())
767 }
768
769 async fn delete_sketch(
770 &mut self,
771 ctx: &ExecutorContext,
772 _version: Version,
773 sketch: ObjectId,
774 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
775 let mut new_ast = self.program.ast.clone();
778
779 let sketch_id = sketch;
781 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
782 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
783 })?;
784 let ObjectKind::Sketch(_) = &sketch_object.kind else {
785 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
786 "Object is not a sketch, it is {}",
787 sketch_object.kind.human_friendly_kind_with_article(),
788 ))));
789 };
790
791 self.mutate_ast(&mut new_ast, sketch_id, AstMutateCommand::DeleteNode)
793 .map_err(KclErrorWithOutputs::no_outputs)?;
794
795 self.execute_after_delete_sketch(ctx, &mut new_ast).await
796 }
797
798 async fn add_segment(
799 &mut self,
800 ctx: &ExecutorContext,
801 _version: Version,
802 sketch: ObjectId,
803 segment: SegmentCtor,
804 _label: Option<String>,
805 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
806 match segment {
808 SegmentCtor::Point(ctor) => self.add_point(ctx, sketch, ctor).await,
809 SegmentCtor::Line(ctor) => self.add_line(ctx, sketch, ctor).await,
810 SegmentCtor::Arc(ctor) => self.add_arc(ctx, sketch, ctor).await,
811 SegmentCtor::Circle(ctor) => self.add_circle(ctx, sketch, ctor).await,
812 SegmentCtor::ControlPointSpline(ctor) => self.add_control_point_spline(ctx, sketch, ctor).await,
813 }
814 }
815
816 async fn edit_segments(
817 &mut self,
818 ctx: &ExecutorContext,
819 _version: Version,
820 sketch: ObjectId,
821 segments: Vec<ExistingSegmentCtor>,
822 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
823 let sketch_block_ref =
825 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
826
827 let mut new_ast = self.program.ast.clone();
828 let mut edited_segment_ids = AhashIndexSet::with_capacity_and_hasher(segments.len(), Default::default());
829 let mut invalidates_ids = false;
830
831 for segment in &segments {
834 edited_segment_ids.insert(segment.id);
835 if let SegmentCtor::ControlPointSpline(new_ctor) = &segment.ctor
836 && let Some(existing_object) = self.scene_graph.objects.get(segment.id.0)
837 && let ObjectKind::Segment {
838 segment: Segment::ControlPointSpline(existing_spline),
839 } = &existing_object.kind
840 && existing_spline.controls.len() != new_ctor.points.len()
841 {
842 invalidates_ids = true;
843 }
844 }
845 let drag_anchor_segment_ids = self
846 .next_drag_anchor_segment_ids
847 .take()
848 .unwrap_or_else(|| edited_segment_ids.clone());
849 let commit_solved_initial_guesses = self.next_edit_commits_solver_solutions.take().unwrap_or(true);
850
851 let mut final_edits: IndexMap<ObjectId, SegmentCtor> = IndexMap::new();
866
867 for segment in segments {
868 let segment_id = segment.id;
869 match segment.ctor {
870 SegmentCtor::Point(ctor) => {
871 if let Some(segment_object) = self.scene_graph.objects.get(segment_id.0)
873 && let ObjectKind::Segment { segment } = &segment_object.kind
874 && let Segment::Point(point) = segment
875 && let Some(owner_id) = point.owner
876 && let Some(owner_object) = self.scene_graph.objects.get(owner_id.0)
877 && let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind
878 {
879 match owner_segment {
880 Segment::Line(line) if line.start == segment_id || line.end == segment_id => {
881 if let Some(existing) = final_edits.get_mut(&owner_id) {
882 let SegmentCtor::Line(line_ctor) = existing else {
883 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
884 "Internal: Expected line ctor for owner, but found {}",
885 existing.human_friendly_kind_with_article()
886 ))));
887 };
888 if line.start == segment_id {
890 line_ctor.start = ctor.position;
891 } else {
892 line_ctor.end = ctor.position;
893 }
894 } else if let SegmentCtor::Line(line_ctor) = &line.ctor {
895 let mut line_ctor = line_ctor.clone();
897 if line.start == segment_id {
898 line_ctor.start = ctor.position;
899 } else {
900 line_ctor.end = ctor.position;
901 }
902 final_edits.insert(owner_id, SegmentCtor::Line(line_ctor));
903 } else {
904 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
906 "Internal: Line does not have line ctor, but found {}",
907 line.ctor.human_friendly_kind_with_article()
908 ))));
909 }
910 continue;
911 }
912 Segment::Arc(arc)
913 if arc.start == segment_id || arc.end == segment_id || arc.center == segment_id =>
914 {
915 if let Some(existing) = final_edits.get_mut(&owner_id) {
916 let SegmentCtor::Arc(arc_ctor) = existing else {
917 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
918 "Internal: Expected arc ctor for owner, but found {}",
919 existing.human_friendly_kind_with_article()
920 ))));
921 };
922 if arc.start == segment_id {
923 arc_ctor.start = ctor.position;
924 } else if arc.end == segment_id {
925 arc_ctor.end = ctor.position;
926 } else {
927 arc_ctor.center = ctor.position;
928 }
929 } else if let SegmentCtor::Arc(arc_ctor) = &arc.ctor {
930 let mut arc_ctor = arc_ctor.clone();
931 if arc.start == segment_id {
932 arc_ctor.start = ctor.position;
933 } else if arc.end == segment_id {
934 arc_ctor.end = ctor.position;
935 } else {
936 arc_ctor.center = ctor.position;
937 }
938 final_edits.insert(owner_id, SegmentCtor::Arc(arc_ctor));
939 } else {
940 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
941 "Internal: Arc does not have arc ctor, but found {}",
942 arc.ctor.human_friendly_kind_with_article()
943 ))));
944 }
945 continue;
946 }
947 Segment::Circle(circle) if circle.start == segment_id || circle.center == segment_id => {
948 if let Some(existing) = final_edits.get_mut(&owner_id) {
949 let SegmentCtor::Circle(circle_ctor) = existing else {
950 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
951 "Internal: Expected circle ctor for owner, but found {}",
952 existing.human_friendly_kind_with_article()
953 ))));
954 };
955 if circle.start == segment_id {
956 circle_ctor.start = ctor.position;
957 } else {
958 circle_ctor.center = ctor.position;
959 }
960 } else if let SegmentCtor::Circle(circle_ctor) = &circle.ctor {
961 let mut circle_ctor = circle_ctor.clone();
962 if circle.start == segment_id {
963 circle_ctor.start = ctor.position;
964 } else {
965 circle_ctor.center = ctor.position;
966 }
967 final_edits.insert(owner_id, SegmentCtor::Circle(circle_ctor));
968 } else {
969 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
970 "Internal: Circle does not have circle ctor, but found {}",
971 circle.ctor.human_friendly_kind_with_article()
972 ))));
973 }
974 continue;
975 }
976 Segment::ControlPointSpline(spline) if spline.controls.contains(&segment_id) => {
977 let Some(control_index) =
978 spline.controls.iter().position(|control_id| *control_id == segment_id)
979 else {
980 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
981 "Internal: Point is not part of owner's controlPointSpline segment: point={segment_id:?}, spline={owner_id:?}"
982 ))));
983 };
984 if let Some(existing) = final_edits.get_mut(&owner_id) {
985 let SegmentCtor::ControlPointSpline(spline_ctor) = existing else {
986 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
987 "Internal: Expected controlPointSpline ctor for owner, but found {}",
988 existing.human_friendly_kind_with_article()
989 ))));
990 };
991 spline_ctor.points[control_index] = ctor.position;
992 } else if let SegmentCtor::ControlPointSpline(spline_ctor) = &spline.ctor {
993 let mut spline_ctor = spline_ctor.clone();
994 spline_ctor.points[control_index] = ctor.position;
995 final_edits.insert(owner_id, SegmentCtor::ControlPointSpline(spline_ctor));
996 } else {
997 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
998 "Internal: Control point spline does not have controlPointSpline ctor, but found {}",
999 spline.ctor.human_friendly_kind_with_article()
1000 ))));
1001 }
1002 continue;
1003 }
1004 _ => {}
1005 }
1006 }
1007
1008 final_edits.insert(segment_id, SegmentCtor::Point(ctor));
1010 }
1011 SegmentCtor::Line(ctor) => {
1012 final_edits.insert(segment_id, SegmentCtor::Line(ctor));
1013 }
1014 SegmentCtor::Arc(ctor) => {
1015 final_edits.insert(segment_id, SegmentCtor::Arc(ctor));
1016 }
1017 SegmentCtor::Circle(ctor) => {
1018 final_edits.insert(segment_id, SegmentCtor::Circle(ctor));
1019 }
1020 SegmentCtor::ControlPointSpline(ctor) => {
1021 final_edits.insert(segment_id, SegmentCtor::ControlPointSpline(ctor));
1022 }
1023 }
1024 }
1025
1026 for (segment_id, ctor) in final_edits {
1027 match ctor {
1028 SegmentCtor::Point(ctor) => self
1029 .edit_point(&mut new_ast, sketch, segment_id, ctor)
1030 .map_err(KclErrorWithOutputs::no_outputs)?,
1031 SegmentCtor::Line(ctor) => self
1032 .edit_line(&mut new_ast, sketch, segment_id, ctor)
1033 .map_err(KclErrorWithOutputs::no_outputs)?,
1034 SegmentCtor::Arc(ctor) => self
1035 .edit_arc(&mut new_ast, sketch, segment_id, ctor)
1036 .map_err(KclErrorWithOutputs::no_outputs)?,
1037 SegmentCtor::Circle(ctor) => self
1038 .edit_circle(&mut new_ast, sketch, segment_id, ctor)
1039 .map_err(KclErrorWithOutputs::no_outputs)?,
1040 SegmentCtor::ControlPointSpline(ctor) => self
1041 .edit_control_point_spline(&mut new_ast, sketch, segment_id, ctor)
1042 .map_err(KclErrorWithOutputs::no_outputs)?,
1043 }
1044 }
1045 let (source_delta, mut scene_graph_delta) = self
1046 .execute_after_edit(
1047 ctx,
1048 sketch,
1049 sketch_block_ref,
1050 &mut new_ast,
1051 ExecuteAfterEditOptions {
1052 segment_ids_edited: drag_anchor_segment_ids,
1053 edit_kind: EditDeleteKind::Edit,
1054 commit_solved_initial_guesses,
1055 },
1056 )
1057 .await?;
1058 if invalidates_ids {
1059 scene_graph_delta.invalidates_ids = true;
1060 }
1061 Ok((source_delta, scene_graph_delta))
1062 }
1063
1064 async fn delete_objects(
1065 &mut self,
1066 ctx: &ExecutorContext,
1067 _version: Version,
1068 sketch: ObjectId,
1069 constraint_ids: Vec<ObjectId>,
1070 segment_ids: Vec<ObjectId>,
1071 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1072 let sketch_block_ref =
1074 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1075
1076 let mut constraint_ids_set = constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
1078 let segment_ids_set = segment_ids.into_iter().collect::<AhashIndexSet<_>>();
1079
1080 let mut resolved_segment_ids_to_delete = AhashIndexSet::default();
1083
1084 for segment_id in segment_ids_set.iter().copied() {
1085 let owner_id = self.scene_graph.objects.get(segment_id.0).and_then(|segment_object| {
1086 let ObjectKind::Segment { segment } = &segment_object.kind else {
1087 return None;
1088 };
1089 match segment {
1090 Segment::Point(point) => point.owner,
1091 Segment::Line(line) => line.owner,
1092 _ => None,
1093 }
1094 });
1095
1096 if let Some(owner_id) = owner_id
1097 && let Some(owner_object) = self.scene_graph.objects.get(owner_id.0)
1098 && let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind
1099 && matches!(
1100 owner_segment,
1101 Segment::Line(_) | Segment::Arc(_) | Segment::Circle(_) | Segment::ControlPointSpline(_)
1102 )
1103 {
1104 resolved_segment_ids_to_delete.insert(owner_id);
1106 } else {
1107 resolved_segment_ids_to_delete.insert(segment_id);
1109 }
1110 }
1111 let referenced_constraint_ids = self
1112 .find_referenced_constraints(sketch, &resolved_segment_ids_to_delete)
1113 .map_err(KclErrorWithOutputs::no_outputs)?;
1114
1115 let mut new_ast = self.program.ast.clone();
1116
1117 for constraint_id in referenced_constraint_ids {
1118 if constraint_ids_set.contains(&constraint_id) {
1119 continue;
1120 }
1121
1122 let constraint_object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
1123 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Constraint not found: {constraint_id:?}")))
1124 })?;
1125 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
1126 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1127 "Object is not a constraint, it is {}",
1128 constraint_object.kind.human_friendly_kind_with_article()
1129 ))));
1130 };
1131
1132 match constraint {
1133 Constraint::Coincident(coincident) => {
1134 let remaining_segments =
1135 self.remaining_constraint_segments(&coincident.segments, &resolved_segment_ids_to_delete);
1136
1137 if remaining_segments.len() >= 2 {
1139 self.edit_coincident_constraint(&mut new_ast, constraint_id, remaining_segments)
1140 .map_err(KclErrorWithOutputs::no_outputs)?;
1141 } else {
1142 constraint_ids_set.insert(constraint_id);
1143 }
1144 }
1145 Constraint::EqualRadius(equal_radius) => {
1146 let remaining_input = equal_radius
1147 .input
1148 .iter()
1149 .copied()
1150 .filter(|segment_id| {
1151 !self.segment_will_be_deleted(*segment_id, &resolved_segment_ids_to_delete)
1152 })
1153 .collect::<Vec<_>>();
1154
1155 if remaining_input.len() >= 2 {
1156 self.edit_equal_radius_constraint(&mut new_ast, constraint_id, remaining_input)
1157 .map_err(KclErrorWithOutputs::no_outputs)?;
1158 } else {
1159 constraint_ids_set.insert(constraint_id);
1160 }
1161 }
1162 Constraint::LinesEqualLength(lines_equal_length) => {
1163 let remaining_lines = lines_equal_length
1164 .lines
1165 .iter()
1166 .copied()
1167 .filter(|line_id| !self.segment_will_be_deleted(*line_id, &resolved_segment_ids_to_delete))
1168 .collect::<Vec<_>>();
1169
1170 if remaining_lines.len() >= 2 {
1172 self.edit_equal_length_constraint(&mut new_ast, constraint_id, remaining_lines)
1173 .map_err(KclErrorWithOutputs::no_outputs)?;
1174 } else {
1175 constraint_ids_set.insert(constraint_id);
1176 }
1177 }
1178 Constraint::Parallel(parallel) => {
1179 let remaining_lines = parallel
1180 .lines
1181 .iter()
1182 .copied()
1183 .filter(|line_id| !self.segment_will_be_deleted(*line_id, &resolved_segment_ids_to_delete))
1184 .collect::<Vec<_>>();
1185
1186 if remaining_lines.len() >= 2 {
1187 self.edit_parallel_constraint(&mut new_ast, constraint_id, remaining_lines)
1188 .map_err(KclErrorWithOutputs::no_outputs)?;
1189 } else {
1190 constraint_ids_set.insert(constraint_id);
1191 }
1192 }
1193 Constraint::Horizontal(Horizontal::Points { points }) => {
1194 let remaining_points = self.remaining_constraint_segments(points, &resolved_segment_ids_to_delete);
1195
1196 if remaining_points.len() >= 2 {
1197 self.edit_horizontal_points_constraint(&mut new_ast, constraint_id, remaining_points)
1198 .map_err(KclErrorWithOutputs::no_outputs)?;
1199 } else {
1200 constraint_ids_set.insert(constraint_id);
1201 }
1202 }
1203 Constraint::Vertical(Vertical::Points { points }) => {
1204 let remaining_points = self.remaining_constraint_segments(points, &resolved_segment_ids_to_delete);
1205
1206 if remaining_points.len() >= 2 {
1207 self.edit_vertical_points_constraint(&mut new_ast, constraint_id, remaining_points)
1208 .map_err(KclErrorWithOutputs::no_outputs)?;
1209 } else {
1210 constraint_ids_set.insert(constraint_id);
1211 }
1212 }
1213 Constraint::Fixed(fixed) => {
1214 if fixed.points.iter().any(|fixed_point| {
1215 self.segment_will_be_deleted(fixed_point.point, &resolved_segment_ids_to_delete)
1216 }) {
1217 constraint_ids_set.insert(constraint_id);
1218 }
1219 }
1220 _ => {
1221 constraint_ids_set.insert(constraint_id);
1223 }
1224 }
1225 }
1226
1227 for constraint_id in constraint_ids_set {
1228 self.delete_constraint(&mut new_ast, sketch, constraint_id)
1229 .map_err(KclErrorWithOutputs::no_outputs)?;
1230 }
1231 for segment_id in resolved_segment_ids_to_delete {
1232 self.delete_segment(&mut new_ast, sketch, segment_id)
1233 .map_err(KclErrorWithOutputs::no_outputs)?;
1234 }
1235
1236 self.execute_after_edit(
1237 ctx,
1238 sketch,
1239 sketch_block_ref,
1240 &mut new_ast,
1241 ExecuteAfterEditOptions {
1242 segment_ids_edited: Default::default(),
1243 edit_kind: EditDeleteKind::DeleteNonSketch,
1244 commit_solved_initial_guesses: true,
1245 },
1246 )
1247 .await
1248 }
1249
1250 async fn add_constraint(
1251 &mut self,
1252 ctx: &ExecutorContext,
1253 _version: Version,
1254 sketch: ObjectId,
1255 constraint: Constraint,
1256 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1257 let original_program = self.program.clone();
1261 let original_scene_graph = self.scene_graph.clone();
1262
1263 let mut new_ast = self.program.ast.clone();
1264 let sketch_block_ref = match constraint {
1265 Constraint::Coincident(coincident) => self
1266 .add_coincident(sketch, coincident, &mut new_ast)
1267 .await
1268 .map_err(KclErrorWithOutputs::no_outputs)?,
1269 Constraint::Distance(distance) => self
1270 .add_distance(sketch, distance, &mut new_ast)
1271 .await
1272 .map_err(KclErrorWithOutputs::no_outputs)?,
1273 Constraint::EqualRadius(equal_radius) => self
1274 .add_equal_radius(sketch, equal_radius, &mut new_ast)
1275 .await
1276 .map_err(KclErrorWithOutputs::no_outputs)?,
1277 Constraint::Fixed(fixed) => self
1278 .add_fixed_constraints(sketch, fixed.points, &mut new_ast)
1279 .await
1280 .map_err(KclErrorWithOutputs::no_outputs)?,
1281 Constraint::HorizontalDistance(distance) => self
1282 .add_horizontal_distance(sketch, distance, &mut new_ast)
1283 .await
1284 .map_err(KclErrorWithOutputs::no_outputs)?,
1285 Constraint::VerticalDistance(distance) => self
1286 .add_vertical_distance(sketch, distance, &mut new_ast)
1287 .await
1288 .map_err(KclErrorWithOutputs::no_outputs)?,
1289 Constraint::Horizontal(horizontal) => self
1290 .add_horizontal(sketch, horizontal, &mut new_ast)
1291 .await
1292 .map_err(KclErrorWithOutputs::no_outputs)?,
1293 Constraint::LinesEqualLength(lines_equal_length) => self
1294 .add_lines_equal_length(sketch, lines_equal_length, &mut new_ast)
1295 .await
1296 .map_err(KclErrorWithOutputs::no_outputs)?,
1297 Constraint::Midpoint(midpoint) => self
1298 .add_midpoint(sketch, midpoint, &mut new_ast)
1299 .await
1300 .map_err(KclErrorWithOutputs::no_outputs)?,
1301 Constraint::Parallel(parallel) => self
1302 .add_parallel(sketch, parallel, &mut new_ast)
1303 .await
1304 .map_err(KclErrorWithOutputs::no_outputs)?,
1305 Constraint::Perpendicular(perpendicular) => self
1306 .add_perpendicular(sketch, perpendicular, &mut new_ast)
1307 .await
1308 .map_err(KclErrorWithOutputs::no_outputs)?,
1309 Constraint::Radius(radius) => self
1310 .add_radius(sketch, radius, &mut new_ast)
1311 .await
1312 .map_err(KclErrorWithOutputs::no_outputs)?,
1313 Constraint::Diameter(diameter) => self
1314 .add_diameter(sketch, diameter, &mut new_ast)
1315 .await
1316 .map_err(KclErrorWithOutputs::no_outputs)?,
1317 Constraint::Symmetric(symmetric) => self
1318 .add_symmetric(sketch, symmetric, &mut new_ast)
1319 .await
1320 .map_err(KclErrorWithOutputs::no_outputs)?,
1321 Constraint::Vertical(vertical) => self
1322 .add_vertical(sketch, vertical, &mut new_ast)
1323 .await
1324 .map_err(KclErrorWithOutputs::no_outputs)?,
1325 Constraint::Angle(lines_at_angle) => self
1326 .add_angle(sketch, lines_at_angle, &mut new_ast)
1327 .await
1328 .map_err(KclErrorWithOutputs::no_outputs)?,
1329 Constraint::Tangent(tangent) => self
1330 .add_tangent(sketch, tangent, &mut new_ast)
1331 .await
1332 .map_err(KclErrorWithOutputs::no_outputs)?,
1333 };
1334
1335 let result = self
1336 .execute_after_add_constraint(ctx, sketch, sketch_block_ref, &mut new_ast)
1337 .await;
1338
1339 if result.is_err() {
1341 self.program = original_program;
1342 self.scene_graph = original_scene_graph;
1343 }
1344
1345 result
1346 }
1347
1348 async fn chain_segment(
1349 &mut self,
1350 ctx: &ExecutorContext,
1351 version: Version,
1352 sketch: ObjectId,
1353 previous_segment_end_point_id: ObjectId,
1354 segment: SegmentCtor,
1355 _label: Option<String>,
1356 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1357 let SegmentCtor::Line(line_ctor) = segment else {
1361 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1362 "chain_segment currently only supports Line segments, got {}",
1363 segment.human_friendly_kind_with_article(),
1364 ))));
1365 };
1366
1367 let (_first_src_delta, first_scene_delta) = self.add_line(ctx, sketch, line_ctor).await?;
1369
1370 let new_line_id = first_scene_delta
1373 .new_objects
1374 .iter()
1375 .find(|&obj_id| {
1376 let obj = self.scene_graph.objects.get(obj_id.0);
1377 if let Some(obj) = obj {
1378 matches!(
1379 &obj.kind,
1380 ObjectKind::Segment {
1381 segment: Segment::Line(_)
1382 }
1383 )
1384 } else {
1385 false
1386 }
1387 })
1388 .ok_or_else(|| {
1389 KclErrorWithOutputs::no_outputs(KclError::refactor(
1390 "Failed to find new line segment in scene graph".to_string(),
1391 ))
1392 })?;
1393
1394 let new_line_obj = self.scene_graph.objects.get(new_line_id.0).ok_or_else(|| {
1395 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1396 "New line object not found: {new_line_id:?}"
1397 )))
1398 })?;
1399
1400 let ObjectKind::Segment {
1401 segment: new_line_segment,
1402 } = &new_line_obj.kind
1403 else {
1404 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1405 "Object is not a segment: {new_line_obj:?}"
1406 ))));
1407 };
1408
1409 let Segment::Line(new_line) = new_line_segment else {
1410 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1411 "Segment is not a line: {new_line_segment:?}"
1412 ))));
1413 };
1414
1415 let new_line_start_point_id = new_line.start;
1416
1417 let coincident = Coincident {
1419 segments: vec![previous_segment_end_point_id.into(), new_line_start_point_id.into()],
1420 };
1421
1422 let (final_src_delta, final_scene_delta) = self
1423 .add_constraint(ctx, version, sketch, Constraint::Coincident(coincident))
1424 .await?;
1425
1426 let mut combined_new_objects = first_scene_delta.new_objects.clone();
1429 combined_new_objects.extend(final_scene_delta.new_objects);
1430
1431 let scene_graph_delta = SceneGraphDelta {
1432 new_graph: self.scene_graph_for_ui(),
1433 invalidates_ids: false,
1434 new_objects: combined_new_objects,
1435 exec_outcome: final_scene_delta.exec_outcome,
1436 };
1437
1438 Ok((final_src_delta, scene_graph_delta))
1439 }
1440
1441 async fn edit_constraint(
1442 &mut self,
1443 ctx: &ExecutorContext,
1444 _version: Version,
1445 sketch: ObjectId,
1446 constraint_id: ObjectId,
1447 value_expression: String,
1448 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1449 let sketch_block_ref =
1451 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1452
1453 let object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
1454 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Object not found: {constraint_id:?}")))
1455 })?;
1456 if !matches!(&object.kind, ObjectKind::Constraint { .. }) {
1457 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1458 "Object is not a constraint: {constraint_id:?}"
1459 ))));
1460 }
1461
1462 let mut new_ast = self.program.ast.clone();
1463
1464 let (parsed, errors) = Program::parse(&value_expression)
1466 .map_err(|e| KclErrorWithOutputs::no_outputs(KclError::refactor(e.to_string())))?;
1467 if !errors.is_empty() {
1468 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1469 "Error parsing value expression: {errors:?}"
1470 ))));
1471 }
1472 let mut parsed = parsed.ok_or_else(|| {
1473 KclErrorWithOutputs::no_outputs(KclError::refactor("No AST produced from value expression".to_string()))
1474 })?;
1475 if parsed.ast.body.is_empty() {
1476 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1477 "Empty value expression".to_string(),
1478 )));
1479 }
1480 let first = parsed.ast.body.remove(0);
1481 let ast::BodyItem::ExpressionStatement(expr_stmt) = first else {
1482 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1483 "Value expression must be a simple expression".to_string(),
1484 )));
1485 };
1486
1487 let new_value: ast::BinaryPart = expr_stmt
1488 .inner
1489 .expression
1490 .try_into()
1491 .map_err(|e: String| KclErrorWithOutputs::no_outputs(KclError::refactor(e)))?;
1492
1493 self.mutate_ast(
1494 &mut new_ast,
1495 constraint_id,
1496 AstMutateCommand::EditConstraintValue { value: new_value },
1497 )
1498 .map_err(KclErrorWithOutputs::no_outputs)?;
1499
1500 self.execute_after_edit(
1501 ctx,
1502 sketch,
1503 sketch_block_ref,
1504 &mut new_ast,
1505 ExecuteAfterEditOptions {
1506 segment_ids_edited: Default::default(),
1507 edit_kind: EditDeleteKind::Edit,
1508 commit_solved_initial_guesses: true,
1509 },
1510 )
1511 .await
1512 }
1513
1514 async fn edit_distance_constraint_label_position(
1515 &mut self,
1516 ctx: &ExecutorContext,
1517 _version: Version,
1518 sketch: ObjectId,
1519 constraint_id: ObjectId,
1520 label_position: Point2d<Number>,
1521 anchor_segment_ids: Vec<ObjectId>,
1522 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1523 let sketch_block_ref =
1525 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1526
1527 let object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
1528 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Object not found: {constraint_id:?}")))
1529 })?;
1530 if !matches!(
1531 &object.kind,
1532 ObjectKind::Constraint {
1533 constraint: Constraint::Distance(_)
1534 | Constraint::HorizontalDistance(_)
1535 | Constraint::VerticalDistance(_)
1536 | Constraint::Radius(_)
1537 | Constraint::Diameter(_),
1538 }
1539 ) {
1540 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1541 "Object does not support labelPosition: {constraint_id:?}"
1542 ))));
1543 }
1544
1545 let label_position = to_ast_point2d_number(&label_position).map_err(|err| {
1546 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1547 "Could not convert label position to AST: {err}"
1548 )))
1549 })?;
1550 let mut new_ast = self.program.ast.clone();
1551 self.mutate_ast(
1552 &mut new_ast,
1553 constraint_id,
1554 AstMutateCommand::EditDistanceConstraintLabelPosition { label_position },
1555 )
1556 .map_err(KclErrorWithOutputs::no_outputs)?;
1557 let commit_solved_initial_guesses = self.next_edit_commits_solver_solutions.take().unwrap_or(true);
1558
1559 self.execute_after_edit(
1560 ctx,
1561 sketch,
1562 sketch_block_ref,
1563 &mut new_ast,
1564 ExecuteAfterEditOptions {
1565 segment_ids_edited: anchor_segment_ids.into_iter().collect(),
1566 edit_kind: EditDeleteKind::Edit,
1567 commit_solved_initial_guesses,
1568 },
1569 )
1570 .await
1571 }
1572
1573 async fn batch_split_segment_operations(
1581 &mut self,
1582 ctx: &ExecutorContext,
1583 _version: Version,
1584 sketch: ObjectId,
1585 edit_segments: Vec<ExistingSegmentCtor>,
1586 add_constraints: Vec<Constraint>,
1587 delete_constraint_ids: Vec<ObjectId>,
1588 _new_segment_info: sketch::NewSegmentInfo,
1589 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1590 let sketch_block_ref =
1592 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1593
1594 let mut new_ast = self.program.ast.clone();
1595 let mut segment_ids_edited = AhashIndexSet::with_capacity_and_hasher(edit_segments.len(), Default::default());
1596
1597 for segment in edit_segments {
1599 segment_ids_edited.insert(segment.id);
1600 match segment.ctor {
1601 SegmentCtor::Point(ctor) => self
1602 .edit_point(&mut new_ast, sketch, segment.id, ctor)
1603 .map_err(KclErrorWithOutputs::no_outputs)?,
1604 SegmentCtor::Line(ctor) => self
1605 .edit_line(&mut new_ast, sketch, segment.id, ctor)
1606 .map_err(KclErrorWithOutputs::no_outputs)?,
1607 SegmentCtor::Arc(ctor) => self
1608 .edit_arc(&mut new_ast, sketch, segment.id, ctor)
1609 .map_err(KclErrorWithOutputs::no_outputs)?,
1610 SegmentCtor::Circle(ctor) => self
1611 .edit_circle(&mut new_ast, sketch, segment.id, ctor)
1612 .map_err(KclErrorWithOutputs::no_outputs)?,
1613 SegmentCtor::ControlPointSpline(ctor) => self
1614 .edit_control_point_spline(&mut new_ast, sketch, segment.id, ctor)
1615 .map_err(KclErrorWithOutputs::no_outputs)?,
1616 }
1617 }
1618
1619 for constraint in add_constraints {
1621 match constraint {
1622 Constraint::Coincident(coincident) => {
1623 self.add_coincident(sketch, coincident, &mut new_ast)
1624 .await
1625 .map_err(KclErrorWithOutputs::no_outputs)?;
1626 }
1627 Constraint::Distance(distance) => {
1628 self.add_distance(sketch, distance, &mut new_ast)
1629 .await
1630 .map_err(KclErrorWithOutputs::no_outputs)?;
1631 }
1632 Constraint::EqualRadius(equal_radius) => {
1633 self.add_equal_radius(sketch, equal_radius, &mut new_ast)
1634 .await
1635 .map_err(KclErrorWithOutputs::no_outputs)?;
1636 }
1637 Constraint::Fixed(fixed) => {
1638 self.add_fixed_constraints(sketch, fixed.points, &mut new_ast)
1639 .await
1640 .map_err(KclErrorWithOutputs::no_outputs)?;
1641 }
1642 Constraint::HorizontalDistance(distance) => {
1643 self.add_horizontal_distance(sketch, distance, &mut new_ast)
1644 .await
1645 .map_err(KclErrorWithOutputs::no_outputs)?;
1646 }
1647 Constraint::VerticalDistance(distance) => {
1648 self.add_vertical_distance(sketch, distance, &mut new_ast)
1649 .await
1650 .map_err(KclErrorWithOutputs::no_outputs)?;
1651 }
1652 Constraint::Horizontal(horizontal) => {
1653 self.add_horizontal(sketch, horizontal, &mut new_ast)
1654 .await
1655 .map_err(KclErrorWithOutputs::no_outputs)?;
1656 }
1657 Constraint::LinesEqualLength(lines_equal_length) => {
1658 self.add_lines_equal_length(sketch, lines_equal_length, &mut new_ast)
1659 .await
1660 .map_err(KclErrorWithOutputs::no_outputs)?;
1661 }
1662 Constraint::Midpoint(midpoint) => {
1663 self.add_midpoint(sketch, midpoint, &mut new_ast)
1664 .await
1665 .map_err(KclErrorWithOutputs::no_outputs)?;
1666 }
1667 Constraint::Parallel(parallel) => {
1668 self.add_parallel(sketch, parallel, &mut new_ast)
1669 .await
1670 .map_err(KclErrorWithOutputs::no_outputs)?;
1671 }
1672 Constraint::Perpendicular(perpendicular) => {
1673 self.add_perpendicular(sketch, perpendicular, &mut new_ast)
1674 .await
1675 .map_err(KclErrorWithOutputs::no_outputs)?;
1676 }
1677 Constraint::Vertical(vertical) => {
1678 self.add_vertical(sketch, vertical, &mut new_ast)
1679 .await
1680 .map_err(KclErrorWithOutputs::no_outputs)?;
1681 }
1682 Constraint::Diameter(diameter) => {
1683 self.add_diameter(sketch, diameter, &mut new_ast)
1684 .await
1685 .map_err(KclErrorWithOutputs::no_outputs)?;
1686 }
1687 Constraint::Radius(radius) => {
1688 self.add_radius(sketch, radius, &mut new_ast)
1689 .await
1690 .map_err(KclErrorWithOutputs::no_outputs)?;
1691 }
1692 Constraint::Symmetric(symmetric) => {
1693 self.add_symmetric(sketch, symmetric, &mut new_ast)
1694 .await
1695 .map_err(KclErrorWithOutputs::no_outputs)?;
1696 }
1697 Constraint::Angle(angle) => {
1698 self.add_angle(sketch, angle, &mut new_ast)
1699 .await
1700 .map_err(KclErrorWithOutputs::no_outputs)?;
1701 }
1702 Constraint::Tangent(tangent) => {
1703 self.add_tangent(sketch, tangent, &mut new_ast)
1704 .await
1705 .map_err(KclErrorWithOutputs::no_outputs)?;
1706 }
1707 }
1708 }
1709
1710 let constraint_ids_set = delete_constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
1712
1713 let has_constraint_deletions = !constraint_ids_set.is_empty();
1714 for constraint_id in constraint_ids_set {
1715 self.delete_constraint(&mut new_ast, sketch, constraint_id)
1716 .map_err(KclErrorWithOutputs::no_outputs)?;
1717 }
1718
1719 let (source_delta, mut scene_graph_delta) = self
1723 .execute_after_edit(
1724 ctx,
1725 sketch,
1726 sketch_block_ref,
1727 &mut new_ast,
1728 ExecuteAfterEditOptions {
1729 segment_ids_edited,
1730 edit_kind: EditDeleteKind::Edit,
1731 commit_solved_initial_guesses: true,
1732 },
1733 )
1734 .await?;
1735
1736 if has_constraint_deletions {
1739 scene_graph_delta.invalidates_ids = true;
1740 }
1741
1742 Ok((source_delta, scene_graph_delta))
1743 }
1744
1745 async fn batch_tail_cut_operations(
1746 &mut self,
1747 ctx: &ExecutorContext,
1748 _version: Version,
1749 sketch: ObjectId,
1750 edit_segments: Vec<ExistingSegmentCtor>,
1751 add_constraints: Vec<Constraint>,
1752 delete_constraint_ids: Vec<ObjectId>,
1753 additional_edited_segment_ids: Vec<ObjectId>,
1754 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1755 let sketch_block_ref =
1756 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1757
1758 let mut new_ast = self.program.ast.clone();
1759 let mut segment_ids_edited = AhashIndexSet::with_capacity_and_hasher(edit_segments.len(), Default::default());
1760
1761 for segment in edit_segments {
1763 segment_ids_edited.insert(segment.id);
1764 match segment.ctor {
1765 SegmentCtor::Point(ctor) => self
1766 .edit_point(&mut new_ast, sketch, segment.id, ctor)
1767 .map_err(KclErrorWithOutputs::no_outputs)?,
1768 SegmentCtor::Line(ctor) => self
1769 .edit_line(&mut new_ast, sketch, segment.id, ctor)
1770 .map_err(KclErrorWithOutputs::no_outputs)?,
1771 SegmentCtor::Arc(ctor) => self
1772 .edit_arc(&mut new_ast, sketch, segment.id, ctor)
1773 .map_err(KclErrorWithOutputs::no_outputs)?,
1774 SegmentCtor::Circle(ctor) => self
1775 .edit_circle(&mut new_ast, sketch, segment.id, ctor)
1776 .map_err(KclErrorWithOutputs::no_outputs)?,
1777 SegmentCtor::ControlPointSpline(ctor) => self
1778 .edit_control_point_spline(&mut new_ast, sketch, segment.id, ctor)
1779 .map_err(KclErrorWithOutputs::no_outputs)?,
1780 }
1781 }
1782
1783 segment_ids_edited.extend(additional_edited_segment_ids);
1784
1785 for constraint in add_constraints {
1787 match constraint {
1788 Constraint::Coincident(coincident) => {
1789 self.add_coincident(sketch, coincident, &mut new_ast)
1790 .await
1791 .map_err(KclErrorWithOutputs::no_outputs)?;
1792 }
1793 other => {
1794 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1795 "unsupported constraint in tail cut batch: {other:?}"
1796 ))));
1797 }
1798 }
1799 }
1800
1801 let constraint_ids_set = delete_constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
1803
1804 let has_constraint_deletions = !constraint_ids_set.is_empty();
1805 for constraint_id in constraint_ids_set {
1806 self.delete_constraint(&mut new_ast, sketch, constraint_id)
1807 .map_err(KclErrorWithOutputs::no_outputs)?;
1808 }
1809
1810 let (source_delta, mut scene_graph_delta) = self
1814 .execute_after_edit(
1815 ctx,
1816 sketch,
1817 sketch_block_ref,
1818 &mut new_ast,
1819 ExecuteAfterEditOptions {
1820 segment_ids_edited,
1821 edit_kind: EditDeleteKind::Edit,
1822 commit_solved_initial_guesses: true,
1823 },
1824 )
1825 .await?;
1826
1827 if has_constraint_deletions {
1830 scene_graph_delta.invalidates_ids = true;
1831 }
1832
1833 Ok((source_delta, scene_graph_delta))
1834 }
1835}
1836
1837impl FrontendState {
1838 pub async fn hack_set_program(&mut self, ctx: &ExecutorContext, program: Program) -> ExecResult<SetProgramOutcome> {
1839 self.program = program.clone();
1840
1841 self.point_freedom_cache.clear();
1852 match ctx.run_with_caching(program).await {
1853 Ok(outcome) => {
1854 let outcome = self.update_state_after_exec(outcome, true);
1855 let checkpoint_id = self
1856 .create_sketch_checkpoint(outcome.clone())
1857 .await
1858 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
1859 Ok(SetProgramOutcome::Success {
1860 scene_graph: Box::new(self.scene_graph_for_ui()),
1861 exec_outcome: Box::new(outcome),
1862 checkpoint_id: Some(checkpoint_id),
1863 })
1864 }
1865 Err(mut err) => {
1866 let outcome = self.exec_outcome_from_exec_error(err.clone())?;
1869 self.update_state_after_exec(outcome, true);
1870 err.scene_graph = Some(self.scene_graph_for_ui());
1871 Ok(SetProgramOutcome::ExecFailure { error: Box::new(err) })
1872 }
1873 }
1874 }
1875
1876 pub async fn engine_execute(
1879 &mut self,
1880 ctx: &ExecutorContext,
1881 program: Program,
1882 ) -> Result<SceneGraphDelta, KclErrorWithOutputs> {
1883 self.program = program.clone();
1884
1885 self.point_freedom_cache.clear();
1889 match ctx.run_with_caching(program).await {
1890 Ok(outcome) => {
1891 let outcome = self.update_state_after_exec(outcome, true);
1892 Ok(SceneGraphDelta {
1893 new_graph: self.scene_graph_for_ui(),
1894 exec_outcome: outcome,
1895 new_objects: Default::default(),
1897 invalidates_ids: Default::default(),
1899 })
1900 }
1901 Err(mut err) => {
1902 let outcome = self.exec_outcome_from_exec_error(err.clone())?;
1904 self.update_state_after_exec(outcome, true);
1905 err.scene_graph = Some(self.scene_graph_for_ui());
1906 Err(err)
1907 }
1908 }
1909 }
1910
1911 fn exec_outcome_from_exec_error(&self, err: KclErrorWithOutputs) -> Result<ExecOutcome, KclErrorWithOutputs> {
1912 if matches!(err.error, KclError::EngineHangup { .. }) {
1913 return Err(err);
1917 }
1918
1919 let KclErrorWithOutputs {
1920 error,
1921 mut non_fatal,
1922 variables,
1923 operations,
1924 artifact_graph,
1925 scene_objects,
1926 source_range_to_object,
1927 var_solutions,
1928 filenames,
1929 default_planes,
1930 ..
1931 } = err;
1932
1933 if let Some(source_range) = error.source_ranges().first() {
1934 non_fatal.push(CompilationIssue::fatal(*source_range, error.get_message()));
1935 } else {
1936 non_fatal.push(CompilationIssue::fatal(SourceRange::synthetic(), error.get_message()));
1937 }
1938
1939 Ok(ExecOutcome {
1940 variables,
1941 filenames,
1942 operations,
1943 artifact_graph,
1944 scene_objects,
1945 source_range_to_object,
1946 var_solutions,
1947 issues: non_fatal,
1948 default_planes,
1949 })
1950 }
1951
1952 async fn add_point(
1953 &mut self,
1954 ctx: &ExecutorContext,
1955 sketch: ObjectId,
1956 ctor: PointCtor,
1957 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1958 let at_ast = to_ast_point2d(&ctor.position)
1960 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1961 let point_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
1962 callee: ast::Node::no_src(ast_sketch2_name(POINT_FN)),
1963 unlabeled: None,
1964 arguments: vec![ast::LabeledArg {
1965 label: Some(ast::Identifier::new(POINT_AT_PARAM)),
1966 arg: at_ast,
1967 }],
1968 digest: None,
1969 non_code_meta: Default::default(),
1970 })));
1971
1972 let sketch_id = sketch;
1974 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
1975 #[cfg(target_arch = "wasm32")]
1976 web_sys::console::error_1(
1977 &format!(
1978 "Sketch not found; sketch_id={sketch_id:?}, self.scene_graph.objects={:#?}",
1979 &self.scene_graph.objects
1980 )
1981 .into(),
1982 );
1983 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
1984 })?;
1985 let ObjectKind::Sketch(_) = &sketch_object.kind else {
1986 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1987 "Object is not a sketch, it is {}",
1988 sketch_object.kind.human_friendly_kind_with_article(),
1989 ))));
1990 };
1991 let mut new_ast = self.program.ast.clone();
1993 let (sketch_block_ref, _) = self
1994 .mutate_ast(
1995 &mut new_ast,
1996 sketch_id,
1997 AstMutateCommand::AddSketchBlockExprStmt { expr: point_ast },
1998 )
1999 .map_err(KclErrorWithOutputs::no_outputs)?;
2000 let new_source = source_from_ast(&new_ast);
2002 let (new_program, errors) = Program::parse(&new_source)
2004 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2005 if !errors.is_empty() {
2006 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2007 "Error parsing KCL source after adding point: {errors:?}"
2008 ))));
2009 }
2010 let Some(new_program) = new_program else {
2011 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
2012 "No AST produced after adding point".to_string(),
2013 )));
2014 };
2015
2016 let point_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2017 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2018 "Source range of point not found in sketch block: {sketch_block_ref:?}; {err:?}"
2019 )))
2020 })?;
2021
2022 self.program = new_program.clone();
2024
2025 let mut truncated_program = new_program;
2027 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2028 .map_err(KclErrorWithOutputs::no_outputs)?;
2029
2030 let outcome = ctx
2032 .run_mock(
2033 &truncated_program,
2034 &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2035 )
2036 .await?;
2037
2038 let new_object_ids = {
2039 let make_err =
2040 |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2041 let segment_id = outcome
2042 .source_range_to_object
2043 .get(&point_node_ref.range)
2044 .copied()
2045 .ok_or_else(|| make_err(format!("Source range of point not found: {point_node_ref:?}")))?;
2046 let segment_object = outcome
2047 .scene_objects
2048 .get(segment_id.0)
2049 .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2050 let ObjectKind::Segment { segment } = &segment_object.kind else {
2051 return Err(make_err(format!(
2052 "Object is not a segment, it is {}",
2053 segment_object.kind.human_friendly_kind_with_article()
2054 )));
2055 };
2056 let Segment::Point(_) = segment else {
2057 return Err(make_err(format!(
2058 "Segment is not a point, it is {}",
2059 segment.human_friendly_kind_with_article()
2060 )));
2061 };
2062 vec![segment_id]
2063 };
2064 let src_delta = SourceDelta { text: new_source };
2065 let outcome = self.update_state_after_exec(outcome, false);
2067 let scene_graph_delta = SceneGraphDelta {
2068 new_graph: self.scene_graph_for_ui(),
2069 invalidates_ids: false,
2070 new_objects: new_object_ids,
2071 exec_outcome: outcome,
2072 };
2073 Ok((src_delta, scene_graph_delta))
2074 }
2075
2076 async fn add_line(
2077 &mut self,
2078 ctx: &ExecutorContext,
2079 sketch: ObjectId,
2080 ctor: LineCtor,
2081 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2082 let start_ast = to_ast_point2d(&ctor.start)
2084 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2085 let end_ast = to_ast_point2d(&ctor.end)
2086 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2087 let mut arguments = vec![
2088 ast::LabeledArg {
2089 label: Some(ast::Identifier::new(LINE_START_PARAM)),
2090 arg: start_ast,
2091 },
2092 ast::LabeledArg {
2093 label: Some(ast::Identifier::new(LINE_END_PARAM)),
2094 arg: end_ast,
2095 },
2096 ];
2097 if ctor.construction == Some(true) {
2099 arguments.push(ast::LabeledArg {
2100 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2101 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
2102 value: ast::LiteralValue::Bool(true),
2103 raw: "true".to_string(),
2104 digest: None,
2105 }))),
2106 });
2107 }
2108 let line_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
2109 callee: ast::Node::no_src(ast_sketch2_name(LINE_FN)),
2110 unlabeled: None,
2111 arguments,
2112 digest: None,
2113 non_code_meta: Default::default(),
2114 })));
2115
2116 let sketch_id = sketch;
2118 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2119 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2120 })?;
2121 let ObjectKind::Sketch(_) = &sketch_object.kind else {
2122 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2123 "Object is not a sketch, it is {}",
2124 sketch_object.kind.human_friendly_kind_with_article(),
2125 ))));
2126 };
2127 let mut new_ast = self.program.ast.clone();
2129 let (sketch_block_ref, _) = self
2130 .mutate_ast(
2131 &mut new_ast,
2132 sketch_id,
2133 AstMutateCommand::AddSketchBlockExprStmt { expr: line_ast },
2134 )
2135 .map_err(KclErrorWithOutputs::no_outputs)?;
2136 let new_source = source_from_ast(&new_ast);
2138 let (new_program, errors) = Program::parse(&new_source)
2140 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2141 if !errors.is_empty() {
2142 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2143 "Error parsing KCL source after adding line: {errors:?}"
2144 ))));
2145 }
2146 let Some(new_program) = new_program else {
2147 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
2148 "No AST produced after adding line".to_string(),
2149 )));
2150 };
2151
2152 let line_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2153 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2154 "Source range of line not found in sketch block: {sketch_block_ref:?}; {err:?}"
2155 )))
2156 })?;
2157
2158 self.program = new_program.clone();
2160
2161 let mut truncated_program = new_program;
2163 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2164 .map_err(KclErrorWithOutputs::no_outputs)?;
2165
2166 let outcome = ctx
2168 .run_mock(
2169 &truncated_program,
2170 &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2171 )
2172 .await?;
2173
2174 let new_object_ids = {
2175 let make_err =
2176 |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2177 let segment_id = outcome
2178 .source_range_to_object
2179 .get(&line_node_ref.range)
2180 .copied()
2181 .ok_or_else(|| make_err(format!("Source range of line not found: {line_node_ref:?}")))?;
2182 let segment_object = outcome
2183 .scene_object_by_id(segment_id)
2184 .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2185 let ObjectKind::Segment { segment } = &segment_object.kind else {
2186 return Err(make_err(format!(
2187 "Object is not a segment, it is {}",
2188 segment_object.kind.human_friendly_kind_with_article()
2189 )));
2190 };
2191 let Segment::Line(line) = segment else {
2192 return Err(make_err(format!(
2193 "Segment is not a line, it is {}",
2194 segment.human_friendly_kind_with_article()
2195 )));
2196 };
2197 vec![line.start, line.end, segment_id]
2198 };
2199 let src_delta = SourceDelta { text: new_source };
2200 let outcome = self.update_state_after_exec(outcome, false);
2202 let scene_graph_delta = SceneGraphDelta {
2203 new_graph: self.scene_graph_for_ui(),
2204 invalidates_ids: false,
2205 new_objects: new_object_ids,
2206 exec_outcome: outcome,
2207 };
2208 Ok((src_delta, scene_graph_delta))
2209 }
2210
2211 async fn add_arc(
2212 &mut self,
2213 ctx: &ExecutorContext,
2214 sketch: ObjectId,
2215 ctor: ArcCtor,
2216 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2217 let start_ast = to_ast_point2d(&ctor.start)
2219 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2220 let end_ast = to_ast_point2d(&ctor.end)
2221 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2222 let center_ast = to_ast_point2d(&ctor.center)
2223 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2224 let mut arguments = vec![
2225 ast::LabeledArg {
2226 label: Some(ast::Identifier::new(ARC_START_PARAM)),
2227 arg: start_ast,
2228 },
2229 ast::LabeledArg {
2230 label: Some(ast::Identifier::new(ARC_END_PARAM)),
2231 arg: end_ast,
2232 },
2233 ast::LabeledArg {
2234 label: Some(ast::Identifier::new(ARC_CENTER_PARAM)),
2235 arg: center_ast,
2236 },
2237 ];
2238 if ctor.construction == Some(true) {
2240 arguments.push(ast::LabeledArg {
2241 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2242 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
2243 value: ast::LiteralValue::Bool(true),
2244 raw: "true".to_string(),
2245 digest: None,
2246 }))),
2247 });
2248 }
2249 let arc_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
2250 callee: ast::Node::no_src(ast_sketch2_name(ARC_FN)),
2251 unlabeled: None,
2252 arguments,
2253 digest: None,
2254 non_code_meta: Default::default(),
2255 })));
2256
2257 let sketch_id = sketch;
2259 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2260 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2261 })?;
2262 let ObjectKind::Sketch(_) = &sketch_object.kind else {
2263 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2264 "Object is not a sketch, it is {}",
2265 sketch_object.kind.human_friendly_kind_with_article(),
2266 ))));
2267 };
2268 let mut new_ast = self.program.ast.clone();
2270 let (sketch_block_ref, _) = self
2271 .mutate_ast(
2272 &mut new_ast,
2273 sketch_id,
2274 AstMutateCommand::AddSketchBlockExprStmt { expr: arc_ast },
2275 )
2276 .map_err(KclErrorWithOutputs::no_outputs)?;
2277 let new_source = source_from_ast(&new_ast);
2279 let (new_program, errors) = Program::parse(&new_source)
2281 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2282 if !errors.is_empty() {
2283 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2284 "Error parsing KCL source after adding arc: {errors:?}"
2285 ))));
2286 }
2287 let Some(new_program) = new_program else {
2288 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
2289 "No AST produced after adding arc".to_string(),
2290 )));
2291 };
2292
2293 let arc_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2294 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2295 "Source range of arc not found in sketch block: {sketch_block_ref:?}; {err:?}"
2296 )))
2297 })?;
2298
2299 self.program = new_program.clone();
2301
2302 let mut truncated_program = new_program;
2304 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2305 .map_err(KclErrorWithOutputs::no_outputs)?;
2306
2307 let outcome = ctx
2309 .run_mock(
2310 &truncated_program,
2311 &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2312 )
2313 .await?;
2314
2315 let new_object_ids = {
2316 let make_err =
2317 |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2318 let segment_id = outcome
2319 .source_range_to_object
2320 .get(&arc_node_ref.range)
2321 .copied()
2322 .ok_or_else(|| make_err(format!("Source range of arc not found: {arc_node_ref:?}")))?;
2323 let segment_object = outcome
2324 .scene_objects
2325 .get(segment_id.0)
2326 .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2327 let ObjectKind::Segment { segment } = &segment_object.kind else {
2328 return Err(make_err(format!(
2329 "Object is not a segment, it is {}",
2330 segment_object.kind.human_friendly_kind_with_article()
2331 )));
2332 };
2333 let Segment::Arc(arc) = segment else {
2334 return Err(make_err(format!(
2335 "Segment is not an arc, it is {}",
2336 segment.human_friendly_kind_with_article()
2337 )));
2338 };
2339 vec![arc.start, arc.end, arc.center, segment_id]
2340 };
2341 let src_delta = SourceDelta { text: new_source };
2342 let outcome = self.update_state_after_exec(outcome, false);
2344 let scene_graph_delta = SceneGraphDelta {
2345 new_graph: self.scene_graph_for_ui(),
2346 invalidates_ids: false,
2347 new_objects: new_object_ids,
2348 exec_outcome: outcome,
2349 };
2350 Ok((src_delta, scene_graph_delta))
2351 }
2352
2353 async fn add_circle(
2354 &mut self,
2355 ctx: &ExecutorContext,
2356 sketch: ObjectId,
2357 ctor: CircleCtor,
2358 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2359 let start_ast = to_ast_point2d(&ctor.start)
2361 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2362 let center_ast = to_ast_point2d(&ctor.center)
2363 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2364 let mut arguments = vec![
2365 ast::LabeledArg {
2366 label: Some(ast::Identifier::new(CIRCLE_START_PARAM)),
2367 arg: start_ast,
2368 },
2369 ast::LabeledArg {
2370 label: Some(ast::Identifier::new(CIRCLE_CENTER_PARAM)),
2371 arg: center_ast,
2372 },
2373 ];
2374 if ctor.construction == Some(true) {
2376 arguments.push(ast::LabeledArg {
2377 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2378 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
2379 value: ast::LiteralValue::Bool(true),
2380 raw: "true".to_string(),
2381 digest: None,
2382 }))),
2383 });
2384 }
2385 let circle_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
2386 callee: ast::Node::no_src(ast_sketch2_name(CIRCLE_FN)),
2387 unlabeled: None,
2388 arguments,
2389 digest: None,
2390 non_code_meta: Default::default(),
2391 })));
2392
2393 let sketch_id = sketch;
2395 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2396 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2397 })?;
2398 let ObjectKind::Sketch(_) = &sketch_object.kind else {
2399 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2400 "Object is not a sketch, it is {}",
2401 sketch_object.kind.human_friendly_kind_with_article(),
2402 ))));
2403 };
2404 let mut new_ast = self.program.ast.clone();
2406 let (sketch_block_ref, _) = self
2407 .mutate_ast(
2408 &mut new_ast,
2409 sketch_id,
2410 AstMutateCommand::AddSketchBlockVarDecl {
2411 prefix: CIRCLE_VARIABLE.to_owned(),
2412 expr: circle_ast,
2413 },
2414 )
2415 .map_err(KclErrorWithOutputs::no_outputs)?;
2416 let new_source = source_from_ast(&new_ast);
2418 let (new_program, errors) = Program::parse(&new_source)
2420 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2421 if !errors.is_empty() {
2422 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2423 "Error parsing KCL source after adding circle: {errors:?}"
2424 ))));
2425 }
2426 let Some(new_program) = new_program else {
2427 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
2428 "No AST produced after adding circle".to_string(),
2429 )));
2430 };
2431
2432 let circle_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2433 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2434 "Source range of circle not found in sketch block: {sketch_block_ref:?}; {err:?}"
2435 )))
2436 })?;
2437
2438 self.program = new_program.clone();
2440
2441 let mut truncated_program = new_program;
2443 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2444 .map_err(KclErrorWithOutputs::no_outputs)?;
2445
2446 let outcome = ctx
2448 .run_mock(
2449 &truncated_program,
2450 &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2451 )
2452 .await?;
2453
2454 let new_object_ids = {
2455 let make_err =
2456 |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2457 let segment_id = outcome
2458 .source_range_to_object
2459 .get(&circle_node_ref.range)
2460 .copied()
2461 .ok_or_else(|| make_err(format!("Source range of circle not found: {circle_node_ref:?}")))?;
2462 let segment_object = outcome
2463 .scene_objects
2464 .get(segment_id.0)
2465 .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2466 let ObjectKind::Segment { segment } = &segment_object.kind else {
2467 return Err(make_err(format!(
2468 "Object is not a segment, it is {}",
2469 segment_object.kind.human_friendly_kind_with_article()
2470 )));
2471 };
2472 let Segment::Circle(circle) = segment else {
2473 return Err(make_err(format!(
2474 "Segment is not a circle, it is {}",
2475 segment.human_friendly_kind_with_article()
2476 )));
2477 };
2478 vec![circle.start, circle.center, segment_id]
2479 };
2480 let src_delta = SourceDelta { text: new_source };
2481 let outcome = self.update_state_after_exec(outcome, false);
2483 let scene_graph_delta = SceneGraphDelta {
2484 new_graph: self.scene_graph_for_ui(),
2485 invalidates_ids: false,
2486 new_objects: new_object_ids,
2487 exec_outcome: outcome,
2488 };
2489 Ok((src_delta, scene_graph_delta))
2490 }
2491
2492 async fn add_control_point_spline(
2493 &mut self,
2494 ctx: &ExecutorContext,
2495 sketch: ObjectId,
2496 ctor: ControlPointSplineCtor,
2497 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2498 let new_program = ensure_control_point_spline_experimental_features(&self.program)
2499 .map_err(KclErrorWithOutputs::no_outputs)?;
2500
2501 let points_ast = to_ast_point2d_array(&ctor.points)
2502 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2503 let mut arguments = vec![ast::LabeledArg {
2504 label: Some(ast::Identifier::new(CONTROL_POINT_SPLINE_POINTS_PARAM)),
2505 arg: points_ast,
2506 }];
2507 if ctor.construction == Some(true) {
2508 arguments.push(ast::LabeledArg {
2509 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2510 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
2511 value: ast::LiteralValue::Bool(true),
2512 raw: "true".to_string(),
2513 digest: None,
2514 }))),
2515 });
2516 }
2517 let spline_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
2518 callee: ast::Node::no_src(ast_sketch2_name(CONTROL_POINT_SPLINE_FN)),
2519 unlabeled: None,
2520 arguments,
2521 digest: None,
2522 non_code_meta: Default::default(),
2523 })));
2524
2525 let sketch_object = self.scene_graph.objects.get(sketch.0).ok_or_else(|| {
2526 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2527 })?;
2528 let ObjectKind::Sketch(_) = &sketch_object.kind else {
2529 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2530 "Object is not a sketch, it is {}",
2531 sketch_object.kind.human_friendly_kind_with_article(),
2532 ))));
2533 };
2534
2535 let mut new_ast = new_program.ast.clone();
2536 let (sketch_block_ref, _) = self
2537 .mutate_ast(
2538 &mut new_ast,
2539 sketch,
2540 AstMutateCommand::AddSketchBlockExprStmt { expr: spline_ast },
2541 )
2542 .map_err(KclErrorWithOutputs::no_outputs)?;
2543 let new_source = source_from_ast(&new_ast);
2544 let (new_program, errors) = Program::parse(&new_source)
2545 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2546 if !errors.is_empty() {
2547 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2548 "Error parsing KCL source after adding controlPointSpline: {errors:?}"
2549 ))));
2550 }
2551 let Some(new_program) = new_program else {
2552 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
2553 "No AST produced after adding controlPointSpline".to_string(),
2554 )));
2555 };
2556
2557 let spline_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2558 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2559 "Source range of controlPointSpline not found in sketch block: {sketch_block_ref:?}; {err:?}"
2560 )))
2561 })?;
2562
2563 self.program = new_program.clone();
2564
2565 let mut truncated_program = new_program;
2566 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2567 .map_err(KclErrorWithOutputs::no_outputs)?;
2568
2569 let outcome = ctx
2570 .run_mock(
2571 &truncated_program,
2572 &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2573 )
2574 .await?;
2575
2576 let new_object_ids = {
2577 let make_err =
2578 |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2579 let segment_id = outcome
2580 .source_range_to_object
2581 .get(&spline_node_ref.range)
2582 .copied()
2583 .ok_or_else(|| {
2584 make_err(format!(
2585 "Source range of controlPointSpline not found: {spline_node_ref:?}"
2586 ))
2587 })?;
2588 let segment_object = outcome
2589 .scene_objects
2590 .get(segment_id.0)
2591 .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2592 let ObjectKind::Segment { segment } = &segment_object.kind else {
2593 return Err(make_err(format!(
2594 "Object is not a segment, it is {}",
2595 segment_object.kind.human_friendly_kind_with_article()
2596 )));
2597 };
2598 let Segment::ControlPointSpline(spline) = segment else {
2599 return Err(make_err(format!(
2600 "Segment is not a control point spline, it is {}",
2601 segment.human_friendly_kind_with_article()
2602 )));
2603 };
2604
2605 let mut ids = outcome
2606 .scene_objects
2607 .iter()
2608 .filter_map(|obj| match &obj.kind {
2609 ObjectKind::Segment {
2610 segment: Segment::Line(line),
2611 } if line.owner == Some(segment_id) => Some(obj.id),
2612 _ => None,
2613 })
2614 .collect::<Vec<_>>();
2615 ids.extend(spline.controls.clone());
2616 ids.push(segment_id);
2617 ids
2618 };
2619 let src_delta = SourceDelta { text: new_source };
2620 let outcome = self.update_state_after_exec(outcome, false);
2621 let scene_graph_delta = SceneGraphDelta {
2622 new_graph: self.scene_graph_for_ui(),
2623 invalidates_ids: false,
2624 new_objects: new_object_ids,
2625 exec_outcome: outcome,
2626 };
2627 Ok((src_delta, scene_graph_delta))
2628 }
2629
2630 fn edit_point(
2631 &mut self,
2632 new_ast: &mut ast::Node<ast::Program>,
2633 sketch: ObjectId,
2634 point: ObjectId,
2635 ctor: PointCtor,
2636 ) -> Result<(), KclError> {
2637 let new_at_ast = to_ast_point2d(&ctor.position).map_err(|err| KclError::refactor(err.to_string()))?;
2639
2640 let sketch_id = sketch;
2642 let sketch_object = self
2643 .scene_graph
2644 .objects
2645 .get(sketch_id.0)
2646 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2647 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2648 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2649 };
2650 sketch.segments.iter().find(|o| **o == point).ok_or_else(|| {
2651 KclError::refactor(format!("Point not found in sketch: point={point:?}, sketch={sketch:?}"))
2652 })?;
2653 let point_id = point;
2655 let point_object = self
2656 .scene_graph
2657 .objects
2658 .get(point_id.0)
2659 .ok_or_else(|| KclError::refactor(format!("Point not found in scene graph: point={point:?}")))?;
2660 let ObjectKind::Segment {
2661 segment: Segment::Point(point),
2662 } = &point_object.kind
2663 else {
2664 return Err(KclError::refactor(format!(
2665 "Object is not a point segment: {point_object:?}"
2666 )));
2667 };
2668
2669 if let Some(owner_id) = point.owner {
2671 let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
2672 KclError::refactor(format!(
2673 "Internal: Owner of point not found in scene graph: owner={owner_id:?}",
2674 ))
2675 })?;
2676 let ObjectKind::Segment { segment } = &owner_object.kind else {
2677 return Err(KclError::refactor(format!(
2678 "Internal: Owner of point is not a segment, but found {}",
2679 owner_object.kind.human_friendly_kind_with_article()
2680 )));
2681 };
2682
2683 if let Segment::Line(line) = segment {
2685 let SegmentCtor::Line(line_ctor) = &line.ctor else {
2686 return Err(KclError::refactor(format!(
2687 "Internal: Owner of point does not have line ctor, but found {}",
2688 line.ctor.human_friendly_kind_with_article()
2689 )));
2690 };
2691 let mut line_ctor = line_ctor.clone();
2692 if line.start == point_id {
2694 line_ctor.start = ctor.position;
2695 } else if line.end == point_id {
2696 line_ctor.end = ctor.position;
2697 } else {
2698 return Err(KclError::refactor(format!(
2699 "Internal: Point is not part of owner's line segment: point={point_id:?}, line={owner_id:?}"
2700 )));
2701 }
2702 return self.edit_line(new_ast, sketch_id, owner_id, line_ctor);
2703 }
2704
2705 if let Segment::Arc(arc) = segment {
2707 let SegmentCtor::Arc(arc_ctor) = &arc.ctor else {
2708 return Err(KclError::refactor(format!(
2709 "Internal: Owner of point does not have arc ctor, but found {}",
2710 arc.ctor.human_friendly_kind_with_article()
2711 )));
2712 };
2713 let mut arc_ctor = arc_ctor.clone();
2714 if arc.center == point_id {
2716 arc_ctor.center = ctor.position;
2717 } else if arc.start == point_id {
2718 arc_ctor.start = ctor.position;
2719 } else if arc.end == point_id {
2720 arc_ctor.end = ctor.position;
2721 } else {
2722 return Err(KclError::refactor(format!(
2723 "Internal: Point is not part of owner's arc segment: point={point_id:?}, arc={owner_id:?}"
2724 )));
2725 }
2726 return self.edit_arc(new_ast, sketch_id, owner_id, arc_ctor);
2727 }
2728
2729 if let Segment::Circle(circle) = segment {
2731 let SegmentCtor::Circle(circle_ctor) = &circle.ctor else {
2732 return Err(KclError::refactor(format!(
2733 "Internal: Owner of point does not have circle ctor, but found {}",
2734 circle.ctor.human_friendly_kind_with_article()
2735 )));
2736 };
2737 let mut circle_ctor = circle_ctor.clone();
2738 if circle.center == point_id {
2739 circle_ctor.center = ctor.position;
2740 } else if circle.start == point_id {
2741 circle_ctor.start = ctor.position;
2742 } else {
2743 return Err(KclError::refactor(format!(
2744 "Internal: Point is not part of owner's circle segment: point={point_id:?}, circle={owner_id:?}"
2745 )));
2746 }
2747 return self.edit_circle(new_ast, sketch_id, owner_id, circle_ctor);
2748 }
2749
2750 if let Segment::ControlPointSpline(spline) = segment {
2751 let SegmentCtor::ControlPointSpline(spline_ctor) = &spline.ctor else {
2752 return Err(KclError::refactor(format!(
2753 "Internal: Owner of point does not have controlPointSpline ctor, but found {}",
2754 spline.ctor.human_friendly_kind_with_article()
2755 )));
2756 };
2757 let mut spline_ctor = spline_ctor.clone();
2758 let Some(control_index) = spline.controls.iter().position(|id| *id == point_id) else {
2759 return Err(KclError::refactor(format!(
2760 "Internal: Point is not part of owner's controlPointSpline segment: point={point_id:?}, spline={owner_id:?}"
2761 )));
2762 };
2763 spline_ctor.points[control_index] = ctor.position;
2764 return self.edit_control_point_spline(new_ast, sketch_id, owner_id, spline_ctor);
2765 }
2766
2767 }
2770
2771 self.mutate_ast(new_ast, point_id, AstMutateCommand::EditPoint { at: new_at_ast })?;
2773 Ok(())
2774 }
2775
2776 fn edit_line(
2777 &mut self,
2778 new_ast: &mut ast::Node<ast::Program>,
2779 sketch: ObjectId,
2780 line: ObjectId,
2781 ctor: LineCtor,
2782 ) -> Result<(), KclError> {
2783 let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2785 let new_end_ast = to_ast_point2d(&ctor.end).map_err(|err| KclError::refactor(err.to_string()))?;
2786
2787 let sketch_id = sketch;
2789 let sketch_object = self
2790 .scene_graph
2791 .objects
2792 .get(sketch_id.0)
2793 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2794 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2795 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2796 };
2797 sketch
2798 .segments
2799 .iter()
2800 .find(|o| **o == line)
2801 .ok_or_else(|| KclError::refactor(format!("Line not found in sketch: line={line:?}, sketch={sketch:?}")))?;
2802 let line_id = line;
2804 let line_object = self
2805 .scene_graph
2806 .objects
2807 .get(line_id.0)
2808 .ok_or_else(|| KclError::refactor(format!("Line not found in scene graph: line={line:?}")))?;
2809 let ObjectKind::Segment { .. } = &line_object.kind else {
2810 let kind = line_object.kind.human_friendly_kind_with_article();
2811 return Err(KclError::refactor(format!(
2812 "This constraint only works on Segments, but you selected {kind}"
2813 )));
2814 };
2815
2816 self.mutate_ast(
2818 new_ast,
2819 line_id,
2820 AstMutateCommand::EditLine {
2821 start: new_start_ast,
2822 end: new_end_ast,
2823 construction: ctor.construction,
2824 },
2825 )?;
2826 Ok(())
2827 }
2828
2829 fn edit_arc(
2830 &mut self,
2831 new_ast: &mut ast::Node<ast::Program>,
2832 sketch: ObjectId,
2833 arc: ObjectId,
2834 ctor: ArcCtor,
2835 ) -> Result<(), KclError> {
2836 let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2838 let new_end_ast = to_ast_point2d(&ctor.end).map_err(|err| KclError::refactor(err.to_string()))?;
2839 let new_center_ast = to_ast_point2d(&ctor.center).map_err(|err| KclError::refactor(err.to_string()))?;
2840
2841 let sketch_id = sketch;
2843 let sketch_object = self
2844 .scene_graph
2845 .objects
2846 .get(sketch_id.0)
2847 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2848 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2849 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2850 };
2851 sketch
2852 .segments
2853 .iter()
2854 .find(|o| **o == arc)
2855 .ok_or_else(|| KclError::refactor(format!("Arc not found in sketch: arc={arc:?}, sketch={sketch:?}")))?;
2856 let arc_id = arc;
2858 let arc_object = self
2859 .scene_graph
2860 .objects
2861 .get(arc_id.0)
2862 .ok_or_else(|| KclError::refactor(format!("Arc not found in scene graph: arc={arc:?}")))?;
2863 let ObjectKind::Segment { .. } = &arc_object.kind else {
2864 return Err(KclError::refactor(format!("Object is not a segment: {arc_object:?}")));
2865 };
2866
2867 self.mutate_ast(
2869 new_ast,
2870 arc_id,
2871 AstMutateCommand::EditArc {
2872 start: new_start_ast,
2873 end: new_end_ast,
2874 center: new_center_ast,
2875 construction: ctor.construction,
2876 },
2877 )?;
2878 Ok(())
2879 }
2880
2881 fn edit_circle(
2882 &mut self,
2883 new_ast: &mut ast::Node<ast::Program>,
2884 sketch: ObjectId,
2885 circle: ObjectId,
2886 ctor: CircleCtor,
2887 ) -> Result<(), KclError> {
2888 let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2890 let new_center_ast = to_ast_point2d(&ctor.center).map_err(|err| KclError::refactor(err.to_string()))?;
2891
2892 let sketch_id = sketch;
2894 let sketch_object = self
2895 .scene_graph
2896 .objects
2897 .get(sketch_id.0)
2898 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2899 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2900 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2901 };
2902 sketch.segments.iter().find(|o| **o == circle).ok_or_else(|| {
2903 KclError::refactor(format!(
2904 "Circle not found in sketch: circle={circle:?}, sketch={sketch:?}"
2905 ))
2906 })?;
2907 let circle_id = circle;
2909 let circle_object = self
2910 .scene_graph
2911 .objects
2912 .get(circle_id.0)
2913 .ok_or_else(|| KclError::refactor(format!("Circle not found in scene graph: circle={circle:?}")))?;
2914 let ObjectKind::Segment { .. } = &circle_object.kind else {
2915 return Err(KclError::refactor(format!(
2916 "Object is not a segment: {circle_object:?}"
2917 )));
2918 };
2919
2920 self.mutate_ast(
2922 new_ast,
2923 circle_id,
2924 AstMutateCommand::EditCircle {
2925 start: new_start_ast,
2926 center: new_center_ast,
2927 construction: ctor.construction,
2928 },
2929 )?;
2930 Ok(())
2931 }
2932
2933 fn edit_control_point_spline(
2934 &mut self,
2935 new_ast: &mut ast::Node<ast::Program>,
2936 sketch: ObjectId,
2937 spline: ObjectId,
2938 ctor: ControlPointSplineCtor,
2939 ) -> Result<(), KclError> {
2940 let points_ast = to_ast_point2d_array(&ctor.points).map_err(|err| KclError::refactor(err.to_string()))?;
2941
2942 let sketch_object = self
2943 .scene_graph
2944 .objects
2945 .get(sketch.0)
2946 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2947 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2948 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2949 };
2950 sketch.segments.iter().find(|o| **o == spline).ok_or_else(|| {
2951 KclError::refactor(format!(
2952 "Control point spline not found in sketch: spline={spline:?}, sketch={sketch:?}"
2953 ))
2954 })?;
2955
2956 let spline_object =
2957 self.scene_graph.objects.get(spline.0).ok_or_else(|| {
2958 KclError::refactor(format!("Control point spline not found in scene graph: {spline:?}"))
2959 })?;
2960 let ObjectKind::Segment { .. } = &spline_object.kind else {
2961 return Err(KclError::refactor(format!(
2962 "Object is not a segment: {spline_object:?}"
2963 )));
2964 };
2965
2966 self.mutate_ast(
2967 new_ast,
2968 spline,
2969 AstMutateCommand::EditControlPointSpline {
2970 points: points_ast,
2971 construction: ctor.construction,
2972 },
2973 )?;
2974 Ok(())
2975 }
2976
2977 fn delete_segment(
2978 &mut self,
2979 new_ast: &mut ast::Node<ast::Program>,
2980 sketch: ObjectId,
2981 segment_id: ObjectId,
2982 ) -> Result<(), KclError> {
2983 let sketch_id = sketch;
2985 let sketch_object = self
2986 .scene_graph
2987 .objects
2988 .get(sketch_id.0)
2989 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2990 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2991 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2992 };
2993 sketch.segments.iter().find(|o| **o == segment_id).ok_or_else(|| {
2994 KclError::refactor(format!(
2995 "Segment not found in sketch: segment={segment_id:?}, sketch={sketch:?}"
2996 ))
2997 })?;
2998 let segment_object =
3000 self.scene_graph.objects.get(segment_id.0).ok_or_else(|| {
3001 KclError::refactor(format!("Segment not found in scene graph: segment={segment_id:?}"))
3002 })?;
3003 let ObjectKind::Segment { .. } = &segment_object.kind else {
3004 return Err(KclError::refactor(format!(
3005 "Object is not a segment, it is {}",
3006 segment_object.kind.human_friendly_kind_with_article()
3007 )));
3008 };
3009
3010 self.mutate_ast(new_ast, segment_id, AstMutateCommand::DeleteNode)?;
3012 Ok(())
3013 }
3014
3015 fn delete_constraint(
3016 &mut self,
3017 new_ast: &mut ast::Node<ast::Program>,
3018 sketch: ObjectId,
3019 constraint_id: ObjectId,
3020 ) -> Result<(), KclError> {
3021 let sketch_id = sketch;
3023 let sketch_object = self
3024 .scene_graph
3025 .objects
3026 .get(sketch_id.0)
3027 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
3028 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
3029 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
3030 };
3031 sketch
3032 .constraints
3033 .iter()
3034 .find(|o| **o == constraint_id)
3035 .ok_or_else(|| {
3036 KclError::refactor(format!(
3037 "Constraint not found in sketch: constraint={constraint_id:?}, sketch={sketch:?}"
3038 ))
3039 })?;
3040 let constraint_object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
3042 KclError::refactor(format!(
3043 "Constraint not found in scene graph: constraint={constraint_id:?}"
3044 ))
3045 })?;
3046 let ObjectKind::Constraint { .. } = &constraint_object.kind else {
3047 return Err(KclError::refactor(format!(
3048 "Object is not a constraint, it is {}",
3049 constraint_object.kind.human_friendly_kind_with_article()
3050 )));
3051 };
3052
3053 self.mutate_ast(new_ast, constraint_id, AstMutateCommand::DeleteNode)?;
3055 Ok(())
3056 }
3057
3058 fn edit_coincident_constraint(
3059 &mut self,
3060 new_ast: &mut ast::Node<ast::Program>,
3061 constraint_id: ObjectId,
3062 segments: Vec<ConstraintSegment>,
3063 ) -> Result<(), KclError> {
3064 if segments.len() < 2 {
3065 return Err(KclError::refactor(format!(
3066 "Coincident constraint must have at least 2 inputs, got {}",
3067 segments.len()
3068 )));
3069 }
3070
3071 let segment_asts = segments
3072 .iter()
3073 .map(|segment| self.coincident_segment_to_ast(segment, new_ast))
3074 .collect::<Result<Vec<_>, _>>()?;
3075
3076 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
3077 elements: segment_asts,
3078 digest: None,
3079 non_code_meta: Default::default(),
3080 })));
3081
3082 self.mutate_ast(
3083 new_ast,
3084 constraint_id,
3085 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3086 )?;
3087 Ok(())
3088 }
3089
3090 fn edit_horizontal_points_constraint(
3091 &mut self,
3092 new_ast: &mut ast::Node<ast::Program>,
3093 constraint_id: ObjectId,
3094 points: Vec<ConstraintSegment>,
3095 ) -> Result<(), KclError> {
3096 self.edit_axis_points_constraint(new_ast, constraint_id, points, "Horizontal")
3097 }
3098
3099 fn edit_vertical_points_constraint(
3100 &mut self,
3101 new_ast: &mut ast::Node<ast::Program>,
3102 constraint_id: ObjectId,
3103 points: Vec<ConstraintSegment>,
3104 ) -> Result<(), KclError> {
3105 self.edit_axis_points_constraint(new_ast, constraint_id, points, "Vertical")
3106 }
3107
3108 fn edit_axis_points_constraint(
3109 &mut self,
3110 new_ast: &mut ast::Node<ast::Program>,
3111 constraint_id: ObjectId,
3112 points: Vec<ConstraintSegment>,
3113 constraint_name: &str,
3114 ) -> Result<(), KclError> {
3115 if points.len() < 2 {
3116 return Err(KclError::refactor(format!(
3117 "{constraint_name} points constraint must have at least 2 points, got {}",
3118 points.len()
3119 )));
3120 }
3121
3122 let point_asts = points
3123 .iter()
3124 .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
3125 .collect::<Result<Vec<_>, _>>()?;
3126
3127 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
3128 elements: point_asts,
3129 digest: None,
3130 non_code_meta: Default::default(),
3131 })));
3132
3133 self.mutate_ast(
3134 new_ast,
3135 constraint_id,
3136 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3137 )?;
3138 Ok(())
3139 }
3140
3141 fn edit_equal_length_constraint(
3143 &mut self,
3144 new_ast: &mut ast::Node<ast::Program>,
3145 constraint_id: ObjectId,
3146 lines: Vec<ObjectId>,
3147 ) -> Result<(), KclError> {
3148 if lines.len() < 2 {
3149 return Err(KclError::refactor(format!(
3150 "Lines equal length constraint must have at least 2 lines, got {}",
3151 lines.len()
3152 )));
3153 }
3154
3155 let line_asts = lines
3156 .iter()
3157 .map(|line_id| {
3158 let line_object = self
3159 .scene_graph
3160 .objects
3161 .get(line_id.0)
3162 .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3163 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3164 let kind = line_object.kind.human_friendly_kind_with_article();
3165 return Err(KclError::refactor(format!(
3166 "This constraint only works on Segments, but you selected {kind}"
3167 )));
3168 };
3169 let Segment::Line(_) = line_segment else {
3170 let kind = line_segment.human_friendly_kind_with_article();
3171 return Err(KclError::refactor(format!(
3172 "Only lines can be made equal length, but you selected {kind}"
3173 )));
3174 };
3175
3176 get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)
3177 })
3178 .collect::<Result<Vec<_>, _>>()?;
3179
3180 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
3181 elements: line_asts,
3182 digest: None,
3183 non_code_meta: Default::default(),
3184 })));
3185
3186 self.mutate_ast(
3187 new_ast,
3188 constraint_id,
3189 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3190 )?;
3191 Ok(())
3192 }
3193
3194 fn edit_parallel_constraint(
3196 &mut self,
3197 new_ast: &mut ast::Node<ast::Program>,
3198 constraint_id: ObjectId,
3199 lines: Vec<ObjectId>,
3200 ) -> Result<(), KclError> {
3201 if lines.len() < 2 {
3202 return Err(KclError::refactor(format!(
3203 "Parallel constraint must have at least 2 lines, got {}",
3204 lines.len()
3205 )));
3206 }
3207
3208 let line_asts = lines
3209 .iter()
3210 .map(|line_id| {
3211 let line_object = self
3212 .scene_graph
3213 .objects
3214 .get(line_id.0)
3215 .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3216 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3217 let kind = line_object.kind.human_friendly_kind_with_article();
3218 return Err(KclError::refactor(format!(
3219 "This constraint only works on Segments, but you selected {kind}"
3220 )));
3221 };
3222 let Segment::Line(_) = line_segment else {
3223 let kind = line_segment.human_friendly_kind_with_article();
3224 return Err(KclError::refactor(format!(
3225 "Only lines can be made parallel, but you selected {kind}"
3226 )));
3227 };
3228
3229 get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)
3230 })
3231 .collect::<Result<Vec<_>, _>>()?;
3232
3233 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
3234 elements: line_asts,
3235 digest: None,
3236 non_code_meta: Default::default(),
3237 })));
3238
3239 self.mutate_ast(
3240 new_ast,
3241 constraint_id,
3242 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3243 )?;
3244 Ok(())
3245 }
3246
3247 fn edit_equal_radius_constraint(
3249 &mut self,
3250 new_ast: &mut ast::Node<ast::Program>,
3251 constraint_id: ObjectId,
3252 input: Vec<ObjectId>,
3253 ) -> Result<(), KclError> {
3254 if input.len() < 2 {
3255 return Err(KclError::refactor(format!(
3256 "equalRadius constraint must have at least 2 segments, got {}",
3257 input.len()
3258 )));
3259 }
3260
3261 let input_asts = input
3262 .iter()
3263 .map(|segment_id| self.equal_radius_segment_id_to_ast_reference(*segment_id, new_ast))
3264 .collect::<Result<Vec<_>, _>>()?;
3265
3266 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
3267 elements: input_asts,
3268 digest: None,
3269 non_code_meta: Default::default(),
3270 })));
3271
3272 self.mutate_ast(
3273 new_ast,
3274 constraint_id,
3275 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3276 )?;
3277 Ok(())
3278 }
3279
3280 async fn execute_after_edit(
3281 &mut self,
3282 ctx: &ExecutorContext,
3283 sketch: ObjectId,
3284 sketch_block_ref: AstNodeRef,
3285 new_ast: &mut ast::Node<ast::Program>,
3286 options: ExecuteAfterEditOptions,
3287 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
3288 let ExecuteAfterEditOptions {
3289 segment_ids_edited,
3290 edit_kind,
3291 commit_solved_initial_guesses,
3292 } = options;
3293
3294 let new_source = source_from_ast(new_ast);
3296 let (new_program, errors) = Program::parse(&new_source)
3298 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
3299 if !errors.is_empty() {
3300 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
3301 "Error parsing KCL source after editing: {errors:?}"
3302 ))));
3303 }
3304 let Some(new_program) = new_program else {
3305 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
3306 "No AST produced after editing".to_string(),
3307 )));
3308 };
3309
3310 self.program = new_program.clone();
3312
3313 let is_delete = edit_kind.is_delete();
3315 let truncated_program = {
3316 let mut truncated_program = new_program;
3317 only_sketch_block(
3318 &mut truncated_program.ast,
3319 &sketch_block_ref,
3320 edit_kind.to_change_kind(),
3321 )
3322 .map_err(KclErrorWithOutputs::no_outputs)?;
3323 truncated_program
3324 };
3325
3326 let drag_anchors = self.next_segment_drag_anchors.take().unwrap_or_default();
3328 let mock_config = MockConfig {
3329 sketch_block_id: Some(sketch),
3330 freedom_analysis: is_delete,
3331 segment_ids_edited: segment_ids_edited.clone(),
3332 drag_anchors,
3333 ..Default::default()
3334 };
3335 let outcome = ctx.run_mock(&truncated_program, &mock_config).await?;
3336
3337 let outcome = self.update_state_after_exec(outcome, is_delete);
3339
3340 let src_delta = if commit_solved_initial_guesses {
3341 self.commit_var_solutions_to_program(&outcome, "editing")?
3342 } else {
3343 SourceDelta { text: new_source }
3344 };
3345 let scene_graph_delta = SceneGraphDelta {
3346 new_graph: self.scene_graph_for_ui(),
3347 invalidates_ids: is_delete,
3348 new_objects: Vec::new(),
3349 exec_outcome: outcome,
3350 };
3351 Ok((src_delta, scene_graph_delta))
3352 }
3353
3354 async fn execute_after_delete_sketch(
3355 &mut self,
3356 ctx: &ExecutorContext,
3357 new_ast: &mut ast::Node<ast::Program>,
3358 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
3359 let new_source = source_from_ast(new_ast);
3361 let (new_program, errors) = Program::parse(&new_source)
3363 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
3364 if !errors.is_empty() {
3365 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
3366 "Error parsing KCL source after editing: {errors:?}"
3367 ))));
3368 }
3369 let Some(new_program) = new_program else {
3370 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
3371 "No AST produced after editing".to_string(),
3372 )));
3373 };
3374
3375 self.program = new_program.clone();
3377
3378 let outcome = ctx.run_with_caching(new_program).await?;
3384 let freedom_analysis_ran = true;
3385
3386 let outcome = self.update_state_after_exec(outcome, freedom_analysis_ran);
3387
3388 let src_delta = SourceDelta { text: new_source };
3389 let scene_graph_delta = SceneGraphDelta {
3390 new_graph: self.scene_graph_for_ui(),
3391 invalidates_ids: true,
3392 new_objects: Vec::new(),
3393 exec_outcome: outcome,
3394 };
3395 Ok((src_delta, scene_graph_delta))
3396 }
3397
3398 fn point_id_to_ast_reference(
3403 &self,
3404 point_id: ObjectId,
3405 new_ast: &mut ast::Node<ast::Program>,
3406 ) -> Result<ast::Expr, KclError> {
3407 let point_object = self
3408 .scene_graph
3409 .objects
3410 .get(point_id.0)
3411 .ok_or_else(|| KclError::refactor(format!("Point not found: {point_id:?}")))?;
3412 let ObjectKind::Segment { segment: point_segment } = &point_object.kind else {
3413 return Err(KclError::refactor(format!("Object is not a segment: {point_object:?}")));
3414 };
3415 let Segment::Point(point) = point_segment else {
3416 return Err(KclError::refactor(format!(
3417 "Only points are currently supported: {point_object:?}"
3418 )));
3419 };
3420
3421 if let Some(owner_id) = point.owner {
3422 let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
3423 KclError::refactor(format!(
3424 "Owner of point not found in scene graph: point={point_id:?}, owner={owner_id:?}"
3425 ))
3426 })?;
3427 let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind else {
3428 return Err(KclError::refactor(format!(
3429 "Owner of point is not a segment, but found {}",
3430 owner_object.kind.human_friendly_kind_with_article()
3431 )));
3432 };
3433
3434 match owner_segment {
3435 Segment::Line(line) => {
3436 let property = if line.start == point_id {
3437 LINE_PROPERTY_START
3438 } else if line.end == point_id {
3439 LINE_PROPERTY_END
3440 } else {
3441 return Err(KclError::refactor(format!(
3442 "Internal: Point is not part of owner's line segment: point={point_id:?}, line={owner_id:?}"
3443 )));
3444 };
3445 get_or_insert_ast_reference(new_ast, &owner_object.source, LINE_VARIABLE, Some(property))
3446 }
3447 Segment::Arc(arc) => {
3448 let property = if arc.start == point_id {
3449 ARC_PROPERTY_START
3450 } else if arc.end == point_id {
3451 ARC_PROPERTY_END
3452 } else if arc.center == point_id {
3453 ARC_PROPERTY_CENTER
3454 } else {
3455 return Err(KclError::refactor(format!(
3456 "Internal: Point is not part of owner's arc segment: point={point_id:?}, arc={owner_id:?}"
3457 )));
3458 };
3459 get_or_insert_ast_reference(new_ast, &owner_object.source, ARC_VARIABLE, Some(property))
3460 }
3461 Segment::Circle(circle) => {
3462 let property = if circle.start == point_id {
3463 CIRCLE_PROPERTY_START
3464 } else if circle.center == point_id {
3465 CIRCLE_PROPERTY_CENTER
3466 } else {
3467 return Err(KclError::refactor(format!(
3468 "Internal: Point is not part of owner's circle segment: point={point_id:?}, circle={owner_id:?}"
3469 )));
3470 };
3471 get_or_insert_ast_reference(new_ast, &owner_object.source, CIRCLE_VARIABLE, Some(property))
3472 }
3473 Segment::ControlPointSpline(spline) => {
3474 let Some(index) = spline.controls.iter().position(|id| *id == point_id) else {
3475 return Err(KclError::refactor(format!(
3476 "Internal: Point is not part of owner's controlPointSpline segment: point={point_id:?}, spline={owner_id:?}"
3477 )));
3478 };
3479 let owner_expr =
3480 get_or_insert_ast_reference(new_ast, &owner_object.source, CONTROL_POINT_SPLINE_FN, None)?;
3481 let controls_expr = create_member_expression(owner_expr, CONTROL_POINT_SPLINE_PROPERTY_CONTROLS);
3482 Ok(create_index_expression(controls_expr, index))
3483 }
3484 _ => Err(KclError::refactor(format!(
3485 "Internal: Owner of point is not a supported segment type for constraints: {owner_segment:?}"
3486 ))),
3487 }
3488 } else {
3489 get_or_insert_ast_reference(new_ast, &point_object.source, "point", None)
3491 }
3492 }
3493
3494 fn line_id_to_ast_reference(
3495 &self,
3496 line_id: ObjectId,
3497 new_ast: &mut ast::Node<ast::Program>,
3498 ) -> Result<ast::Expr, KclError> {
3499 let line_object = self
3500 .scene_graph
3501 .objects
3502 .get(line_id.0)
3503 .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3504 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3505 return Err(KclError::refactor(format!("Object is not a segment: {line_object:?}")));
3506 };
3507 let Segment::Line(line) = line_segment else {
3508 return Err(KclError::refactor(format!(
3509 "Only lines are currently supported: {line_object:?}"
3510 )));
3511 };
3512
3513 if let Some(owner_id) = line.owner {
3514 let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
3515 KclError::refactor(format!(
3516 "Owner of line not found in scene graph: line={line_id:?}, owner={owner_id:?}"
3517 ))
3518 })?;
3519 let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind else {
3520 return Err(KclError::refactor(format!(
3521 "Owner of line is not a segment, but found {}",
3522 owner_object.kind.human_friendly_kind_with_article()
3523 )));
3524 };
3525
3526 match owner_segment {
3527 Segment::ControlPointSpline(spline) => {
3528 let Some(index) = spline
3529 .controls
3530 .windows(2)
3531 .position(|window| window[0] == line.start && window[1] == line.end)
3532 else {
3533 return Err(KclError::refactor(format!(
3534 "Internal: Line is not part of owner's controlPointSpline segment: line={line_id:?}, spline={owner_id:?}"
3535 )));
3536 };
3537 let owner_expr =
3538 get_or_insert_ast_reference(new_ast, &owner_object.source, CONTROL_POINT_SPLINE_FN, None)?;
3539 let edges_expr = create_member_expression(owner_expr, CONTROL_POINT_SPLINE_PROPERTY_EDGES);
3540 Ok(create_index_expression(edges_expr, index))
3541 }
3542 _ => Err(KclError::refactor(format!(
3543 "Internal: Owner of line is not a supported segment type for constraints: {owner_segment:?}"
3544 ))),
3545 }
3546 } else {
3547 get_or_insert_ast_reference(new_ast, &line_object.source, "line", None)
3548 }
3549 }
3550
3551 fn coincident_segment_to_ast(
3552 &self,
3553 segment: &ConstraintSegment,
3554 new_ast: &mut ast::Node<ast::Program>,
3555 ) -> Result<ast::Expr, KclError> {
3556 match segment {
3557 ConstraintSegment::Origin(_) => Ok(ast_name_expr("ORIGIN".to_owned())),
3558 ConstraintSegment::Segment(segment_id) => self.segment_id_to_constraint_ast_reference(*segment_id, new_ast),
3559 }
3560 }
3561
3562 fn segment_id_to_constraint_ast_reference(
3563 &self,
3564 segment_id: ObjectId,
3565 new_ast: &mut ast::Node<ast::Program>,
3566 ) -> Result<ast::Expr, KclError> {
3567 let segment_object = self
3568 .scene_graph
3569 .objects
3570 .get(segment_id.0)
3571 .ok_or_else(|| KclError::refactor(format!("Object not found: {segment_id:?}")))?;
3572 let ObjectKind::Segment { segment } = &segment_object.kind else {
3573 return Err(KclError::refactor(format!(
3574 "Object is not a segment, it is {}",
3575 segment_object.kind.human_friendly_kind_with_article()
3576 )));
3577 };
3578
3579 match segment {
3580 Segment::Point(_) => self.point_id_to_ast_reference(segment_id, new_ast),
3581 Segment::Line(_) => self.line_id_to_ast_reference(segment_id, new_ast),
3582 Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, "arc", None),
3583 Segment::Circle(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, CIRCLE_VARIABLE, None),
3584 Segment::ControlPointSpline(_) => {
3585 get_or_insert_ast_reference(new_ast, &segment_object.source, CONTROL_POINT_SPLINE_FN, None)
3586 }
3587 }
3588 }
3589
3590 fn axis_constraint_segment_to_ast(
3591 &self,
3592 segment: &ConstraintSegment,
3593 new_ast: &mut ast::Node<ast::Program>,
3594 ) -> Result<ast::Expr, KclError> {
3595 match segment {
3596 ConstraintSegment::Origin(_) => Ok(ast_name_expr("ORIGIN".to_owned())),
3597 ConstraintSegment::Segment(point_id) => self.point_id_to_ast_reference(*point_id, new_ast),
3598 }
3599 }
3600
3601 async fn add_coincident(
3602 &mut self,
3603 sketch: ObjectId,
3604 coincident: Coincident,
3605 new_ast: &mut ast::Node<ast::Program>,
3606 ) -> Result<AstNodeRef, KclError> {
3607 let sketch_id = sketch;
3608 for segment in &coincident.segments {
3609 let ConstraintSegment::Segment(segment_id) = segment else {
3610 continue;
3611 };
3612 let Some(segment_object) = self.scene_graph.objects.get(segment_id.0) else {
3613 continue;
3614 };
3615 if matches!(
3616 segment_object.kind,
3617 ObjectKind::Segment {
3618 segment: Segment::ControlPointSpline(_)
3619 }
3620 ) {
3621 return Err(KclError::refactor(
3622 "Coincident with a full controlPointSpline is not supported yet. Constrain a control point or spline edge instead."
3623 .to_owned(),
3624 ));
3625 }
3626 }
3627 let segment_asts = coincident
3628 .segments
3629 .iter()
3630 .map(|segment| self.coincident_segment_to_ast(segment, new_ast))
3631 .collect::<Result<Vec<_>, _>>()?;
3632 if segment_asts.len() < 2 {
3633 return Err(KclError::refactor(format!(
3634 "Coincident constraint must have at least 2 inputs, got {}",
3635 segment_asts.len()
3636 )));
3637 }
3638
3639 let coincident_ast = create_coincident_ast(segment_asts);
3641
3642 let (sketch_block_ref, _) = self.mutate_ast(
3644 new_ast,
3645 sketch_id,
3646 AstMutateCommand::AddSketchBlockExprStmt { expr: coincident_ast },
3647 )?;
3648 Ok(sketch_block_ref)
3649 }
3650
3651 async fn add_distance(
3652 &mut self,
3653 sketch: ObjectId,
3654 distance: Distance,
3655 new_ast: &mut ast::Node<ast::Program>,
3656 ) -> Result<AstNodeRef, KclError> {
3657 let sketch_id = sketch;
3658 let [pt0_ast, pt1_ast] = match distance.points.as_slice() {
3659 [pt0, pt1] => [
3660 self.coincident_segment_to_ast(pt0, new_ast)?,
3661 self.coincident_segment_to_ast(pt1, new_ast)?,
3662 ],
3663 _ => {
3664 return Err(KclError::refactor(format!(
3665 "Distance constraint must have exactly 2 points, got {}",
3666 distance.points.len()
3667 )));
3668 }
3669 };
3670
3671 let arguments = match &distance.label_position {
3672 Some(label_position) => vec![ast::LabeledArg {
3673 label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
3674 arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
3675 }],
3676 None => Default::default(),
3677 };
3678
3679 let distance_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3681 callee: ast::Node::no_src(ast_sketch2_name(DISTANCE_FN)),
3682 unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
3683 ast::ArrayExpression {
3684 elements: vec![pt0_ast, pt1_ast],
3685 digest: None,
3686 non_code_meta: Default::default(),
3687 },
3688 )))),
3689 arguments,
3690 digest: None,
3691 non_code_meta: Default::default(),
3692 })));
3693 let distance_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
3694 left: distance_call_ast,
3695 operator: ast::BinaryOperator::Eq,
3696 right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
3697 value: ast::LiteralValue::Number {
3698 value: distance.distance.value,
3699 suffix: distance.distance.units,
3700 },
3701 raw: format_number_literal(distance.distance.value, distance.distance.units, None).map_err(|_| {
3702 KclError::refactor(format!(
3703 "Could not format numeric suffix: {:?}",
3704 distance.distance.units
3705 ))
3706 })?,
3707 digest: None,
3708 }))),
3709 digest: None,
3710 })));
3711
3712 let (sketch_block_ref, _) = self.mutate_ast(
3714 new_ast,
3715 sketch_id,
3716 AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
3717 )?;
3718 Ok(sketch_block_ref)
3719 }
3720
3721 async fn add_angle(
3722 &mut self,
3723 sketch: ObjectId,
3724 angle: Angle,
3725 new_ast: &mut ast::Node<ast::Program>,
3726 ) -> Result<AstNodeRef, KclError> {
3727 let &[l0_id, l1_id] = angle.lines.as_slice() else {
3728 return Err(KclError::refactor(format!(
3729 "Angle constraint must have exactly 2 lines, got {}",
3730 angle.lines.len()
3731 )));
3732 };
3733 let sketch_id = sketch;
3734
3735 let line0_object = self
3737 .scene_graph
3738 .objects
3739 .get(l0_id.0)
3740 .ok_or_else(|| KclError::refactor(format!("Line not found: {l0_id:?}")))?;
3741 let ObjectKind::Segment { segment: line0_segment } = &line0_object.kind else {
3742 return Err(KclError::refactor(format!("Object is not a segment: {line0_object:?}")));
3743 };
3744 let Segment::Line(_) = line0_segment else {
3745 return Err(KclError::refactor(format!(
3746 "Only lines can be constrained to meet at an angle: {line0_object:?}",
3747 )));
3748 };
3749 let l0_ast = self.line_id_to_ast_reference(l0_id, new_ast)?;
3750
3751 let line1_object = self
3752 .scene_graph
3753 .objects
3754 .get(l1_id.0)
3755 .ok_or_else(|| KclError::refactor(format!("Line not found: {l1_id:?}")))?;
3756 let ObjectKind::Segment { segment: line1_segment } = &line1_object.kind else {
3757 return Err(KclError::refactor(format!("Object is not a segment: {line1_object:?}")));
3758 };
3759 let Segment::Line(_) = line1_segment else {
3760 return Err(KclError::refactor(format!(
3761 "Only lines can be constrained to meet at an angle: {line1_object:?}",
3762 )));
3763 };
3764 let l1_ast = self.line_id_to_ast_reference(l1_id, new_ast)?;
3765
3766 let angle_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3768 callee: ast::Node::no_src(ast_sketch2_name(ANGLE_FN)),
3769 unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
3770 ast::ArrayExpression {
3771 elements: vec![l0_ast, l1_ast],
3772 digest: None,
3773 non_code_meta: Default::default(),
3774 },
3775 )))),
3776 arguments: Default::default(),
3777 digest: None,
3778 non_code_meta: Default::default(),
3779 })));
3780 let angle_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
3781 left: angle_call_ast,
3782 operator: ast::BinaryOperator::Eq,
3783 right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
3784 value: ast::LiteralValue::Number {
3785 value: angle.angle.value,
3786 suffix: angle.angle.units,
3787 },
3788 raw: format_number_literal(angle.angle.value, angle.angle.units, None).map_err(|_| {
3789 KclError::refactor(format!("Could not format numeric suffix: {:?}", angle.angle.units))
3790 })?,
3791 digest: None,
3792 }))),
3793 digest: None,
3794 })));
3795
3796 let (sketch_block_ref, _) = self.mutate_ast(
3798 new_ast,
3799 sketch_id,
3800 AstMutateCommand::AddSketchBlockExprStmt { expr: angle_ast },
3801 )?;
3802 Ok(sketch_block_ref)
3803 }
3804
3805 async fn add_tangent(
3806 &mut self,
3807 sketch: ObjectId,
3808 tangent: Tangent,
3809 new_ast: &mut ast::Node<ast::Program>,
3810 ) -> Result<AstNodeRef, KclError> {
3811 let &[seg0_id, seg1_id] = tangent.input.as_slice() else {
3812 return Err(KclError::refactor(format!(
3813 "Tangent constraint must have exactly 2 segments, got {}",
3814 tangent.input.len()
3815 )));
3816 };
3817 let sketch_id = sketch;
3818
3819 let seg0_object = self
3820 .scene_graph
3821 .objects
3822 .get(seg0_id.0)
3823 .ok_or_else(|| KclError::refactor(format!("Segment not found: {seg0_id:?}")))?;
3824 let ObjectKind::Segment { segment: seg0_segment } = &seg0_object.kind else {
3825 return Err(KclError::refactor(format!("Object is not a segment: {seg0_object:?}")));
3826 };
3827 let seg0_ast = match seg0_segment {
3828 Segment::Line(_) | Segment::Arc(_) | Segment::Circle(_) => {
3829 self.segment_id_to_constraint_ast_reference(seg0_id, new_ast)?
3830 }
3831 _ => {
3832 return Err(KclError::refactor(format!(
3833 "Tangent supports only line/arc/circle segments for now, got: {seg0_segment:?}"
3834 )));
3835 }
3836 };
3837
3838 let seg1_object = self
3839 .scene_graph
3840 .objects
3841 .get(seg1_id.0)
3842 .ok_or_else(|| KclError::refactor(format!("Segment not found: {seg1_id:?}")))?;
3843 let ObjectKind::Segment { segment: seg1_segment } = &seg1_object.kind else {
3844 return Err(KclError::refactor(format!("Object is not a segment: {seg1_object:?}")));
3845 };
3846 let seg1_ast = match seg1_segment {
3847 Segment::Line(_) | Segment::Arc(_) | Segment::Circle(_) => {
3848 self.segment_id_to_constraint_ast_reference(seg1_id, new_ast)?
3849 }
3850 _ => {
3851 return Err(KclError::refactor(format!(
3852 "Tangent supports only line/arc/circle segments for now, got: {seg1_segment:?}"
3853 )));
3854 }
3855 };
3856
3857 let tangent_ast = create_tangent_ast(seg0_ast, seg1_ast);
3858 let (sketch_block_ref, _) = self.mutate_ast(
3859 new_ast,
3860 sketch_id,
3861 AstMutateCommand::AddSketchBlockExprStmt { expr: tangent_ast },
3862 )?;
3863 Ok(sketch_block_ref)
3864 }
3865
3866 async fn add_symmetric(
3867 &mut self,
3868 sketch: ObjectId,
3869 symmetric: Symmetric,
3870 new_ast: &mut ast::Node<ast::Program>,
3871 ) -> Result<AstNodeRef, KclError> {
3872 let &[input0_id, input1_id] = symmetric.input.as_slice() else {
3873 return Err(KclError::refactor(format!(
3874 "Symmetric constraint must have exactly 2 inputs, got {}",
3875 symmetric.input.len()
3876 )));
3877 };
3878 let sketch_id = sketch;
3879
3880 let input0_ast = self.symmetric_input_id_to_ast_reference(input0_id, new_ast)?;
3881 let input1_ast = self.symmetric_input_id_to_ast_reference(input1_id, new_ast)?;
3882 let axis_ast = self.symmetric_axis_id_to_ast_reference(symmetric.axis, new_ast)?;
3883
3884 let symmetric_ast = create_symmetric_ast(vec![input0_ast, input1_ast], axis_ast);
3885 let (sketch_block_ref, _) = self.mutate_ast(
3886 new_ast,
3887 sketch_id,
3888 AstMutateCommand::AddSketchBlockExprStmt { expr: symmetric_ast },
3889 )?;
3890 Ok(sketch_block_ref)
3891 }
3892
3893 async fn add_midpoint(
3894 &mut self,
3895 sketch: ObjectId,
3896 midpoint: Midpoint,
3897 new_ast: &mut ast::Node<ast::Program>,
3898 ) -> Result<AstNodeRef, KclError> {
3899 let sketch_id = sketch;
3900 let point_ast = self.axis_constraint_segment_to_ast(&midpoint.point, new_ast)?;
3901
3902 let segment_object = self
3903 .scene_graph
3904 .objects
3905 .get(midpoint.segment.0)
3906 .ok_or_else(|| KclError::refactor(format!("Segment not found: {:?}", midpoint.segment)))?;
3907 let ObjectKind::Segment {
3908 segment: midpoint_segment,
3909 } = &segment_object.kind
3910 else {
3911 return Err(KclError::refactor(format!(
3912 "Object must be a segment, but it was {}",
3913 segment_object.kind.human_friendly_kind_with_article()
3914 )));
3915 };
3916 let segment_ast = match midpoint_segment {
3917 Segment::Line(_) => self.line_id_to_ast_reference(midpoint.segment, new_ast)?,
3918 Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, "arc", None)?,
3919 _ => {
3920 return Err(KclError::refactor(format!(
3921 "Midpoint target must be a line or arc segment but it was {}",
3922 midpoint_segment.human_friendly_kind_with_article()
3923 )));
3924 }
3925 };
3926
3927 let midpoint_ast = create_midpoint_ast(segment_ast, point_ast);
3928 let (sketch_block_ref, _) = self.mutate_ast(
3929 new_ast,
3930 sketch_id,
3931 AstMutateCommand::AddSketchBlockExprStmt { expr: midpoint_ast },
3932 )?;
3933 Ok(sketch_block_ref)
3934 }
3935
3936 async fn add_equal_radius(
3937 &mut self,
3938 sketch: ObjectId,
3939 equal_radius: EqualRadius,
3940 new_ast: &mut ast::Node<ast::Program>,
3941 ) -> Result<AstNodeRef, KclError> {
3942 if equal_radius.input.len() < 2 {
3943 return Err(KclError::refactor(format!(
3944 "equalRadius constraint must have at least 2 segments, got {}",
3945 equal_radius.input.len()
3946 )));
3947 }
3948
3949 let sketch_id = sketch;
3950 let input_asts = equal_radius
3951 .input
3952 .iter()
3953 .map(|segment_id| self.equal_radius_segment_id_to_ast_reference(*segment_id, new_ast))
3954 .collect::<Result<Vec<_>, _>>()?;
3955
3956 let equal_radius_ast = create_equal_radius_ast(input_asts);
3957 let (sketch_block_ref, _) = self.mutate_ast(
3958 new_ast,
3959 sketch_id,
3960 AstMutateCommand::AddSketchBlockExprStmt { expr: equal_radius_ast },
3961 )?;
3962 Ok(sketch_block_ref)
3963 }
3964
3965 async fn add_radius(
3966 &mut self,
3967 sketch: ObjectId,
3968 radius: Radius,
3969 new_ast: &mut ast::Node<ast::Program>,
3970 ) -> Result<AstNodeRef, KclError> {
3971 let params = ArcSizeConstraintParams {
3972 points: vec![radius.arc],
3973 function_name: RADIUS_FN,
3974 value: radius.radius.value,
3975 units: radius.radius.units,
3976 label_position: radius.label_position,
3977 constraint_type_name: "Radius",
3978 };
3979 self.add_arc_size_constraint(sketch, params, new_ast).await
3980 }
3981
3982 async fn add_diameter(
3983 &mut self,
3984 sketch: ObjectId,
3985 diameter: Diameter,
3986 new_ast: &mut ast::Node<ast::Program>,
3987 ) -> Result<AstNodeRef, KclError> {
3988 let params = ArcSizeConstraintParams {
3989 points: vec![diameter.arc],
3990 function_name: DIAMETER_FN,
3991 value: diameter.diameter.value,
3992 units: diameter.diameter.units,
3993 label_position: diameter.label_position,
3994 constraint_type_name: "Diameter",
3995 };
3996 self.add_arc_size_constraint(sketch, params, new_ast).await
3997 }
3998
3999 async fn add_fixed_constraints(
4000 &mut self,
4001 sketch: ObjectId,
4002 points: Vec<FixedPoint>,
4003 new_ast: &mut ast::Node<ast::Program>,
4004 ) -> Result<AstNodeRef, KclError> {
4005 let mut sketch_block_ref = None;
4006
4007 for fixed_point in points {
4008 let point_ast = self.point_id_to_ast_reference(fixed_point.point, new_ast)?;
4009 let fixed_ast = create_fixed_point_constraint_ast(point_ast, fixed_point.position)
4010 .map_err(|err| KclError::refactor(err.to_string()))?;
4011
4012 let (sketch_ref, _) = self.mutate_ast(
4013 new_ast,
4014 sketch,
4015 AstMutateCommand::AddSketchBlockExprStmt { expr: fixed_ast },
4016 )?;
4017 sketch_block_ref = Some(sketch_ref);
4018 }
4019
4020 sketch_block_ref.ok_or_else(|| KclError::refactor("Fixed constraint requires at least one point".to_owned()))
4021 }
4022
4023 async fn add_arc_size_constraint(
4024 &mut self,
4025 sketch: ObjectId,
4026 params: ArcSizeConstraintParams,
4027 new_ast: &mut ast::Node<ast::Program>,
4028 ) -> Result<AstNodeRef, KclError> {
4029 let sketch_id = sketch;
4030
4031 if params.points.len() != 1 {
4033 return Err(KclError::refactor(format!(
4034 "{} constraint must have exactly 1 argument (an arc segment), got {}",
4035 params.constraint_type_name,
4036 params.points.len()
4037 )));
4038 }
4039
4040 let arc_id = params.points[0];
4041 let arc_object = self
4042 .scene_graph
4043 .objects
4044 .get(arc_id.0)
4045 .ok_or_else(|| KclError::refactor(format!("Arc segment not found: {arc_id:?}")))?;
4046 let ObjectKind::Segment { segment: arc_segment } = &arc_object.kind else {
4047 return Err(KclError::refactor(format!("Object is not a segment: {arc_object:?}")));
4048 };
4049 let ref_type = match arc_segment {
4050 Segment::Arc(_) => ARC_VARIABLE,
4051 Segment::Circle(_) => CIRCLE_VARIABLE,
4052 _ => {
4053 return Err(KclError::refactor(format!(
4054 "{} constraint argument must be an arc or circle segment, got: {arc_segment:?}",
4055 params.constraint_type_name
4056 )));
4057 }
4058 };
4059 let arc_ast = get_or_insert_ast_reference(new_ast, &arc_object.source, ref_type, None)?;
4061 let arguments = match ¶ms.label_position {
4062 Some(label_position) => vec![ast::LabeledArg {
4063 label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
4064 arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
4065 }],
4066 None => Default::default(),
4067 };
4068
4069 let call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
4071 callee: ast::Node::no_src(ast_sketch2_name(params.function_name)),
4072 unlabeled: Some(arc_ast),
4073 arguments,
4074 digest: None,
4075 non_code_meta: Default::default(),
4076 })));
4077 let constraint_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
4078 left: call_ast,
4079 operator: ast::BinaryOperator::Eq,
4080 right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
4081 value: ast::LiteralValue::Number {
4082 value: params.value,
4083 suffix: params.units,
4084 },
4085 raw: format_number_literal(params.value, params.units, None)
4086 .map_err(|_| KclError::refactor(format!("Could not format numeric suffix: {:?}", params.units)))?,
4087 digest: None,
4088 }))),
4089 digest: None,
4090 })));
4091
4092 let (sketch_block_ref, _) = self.mutate_ast(
4094 new_ast,
4095 sketch_id,
4096 AstMutateCommand::AddSketchBlockExprStmt { expr: constraint_ast },
4097 )?;
4098 Ok(sketch_block_ref)
4099 }
4100
4101 async fn add_horizontal_distance(
4102 &mut self,
4103 sketch: ObjectId,
4104 distance: Distance,
4105 new_ast: &mut ast::Node<ast::Program>,
4106 ) -> Result<AstNodeRef, KclError> {
4107 let sketch_id = sketch;
4108 let [pt0_ast, pt1_ast] = match distance.points.as_slice() {
4109 [pt0, pt1] => [
4110 self.coincident_segment_to_ast(pt0, new_ast)?,
4111 self.coincident_segment_to_ast(pt1, new_ast)?,
4112 ],
4113 _ => {
4114 return Err(KclError::refactor(format!(
4115 "Horizontal distance constraint must have exactly 2 points, got {}",
4116 distance.points.len()
4117 )));
4118 }
4119 };
4120
4121 let arguments = match &distance.label_position {
4122 Some(label_position) => vec![ast::LabeledArg {
4123 label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
4124 arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
4125 }],
4126 None => Default::default(),
4127 };
4128
4129 let distance_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
4131 callee: ast::Node::no_src(ast_sketch2_name(HORIZONTAL_DISTANCE_FN)),
4132 unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
4133 ast::ArrayExpression {
4134 elements: vec![pt0_ast, pt1_ast],
4135 digest: None,
4136 non_code_meta: Default::default(),
4137 },
4138 )))),
4139 arguments,
4140 digest: None,
4141 non_code_meta: Default::default(),
4142 })));
4143 let distance_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
4144 left: distance_call_ast,
4145 operator: ast::BinaryOperator::Eq,
4146 right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
4147 value: ast::LiteralValue::Number {
4148 value: distance.distance.value,
4149 suffix: distance.distance.units,
4150 },
4151 raw: format_number_literal(distance.distance.value, distance.distance.units, None).map_err(|_| {
4152 KclError::refactor(format!(
4153 "Could not format numeric suffix: {:?}",
4154 distance.distance.units
4155 ))
4156 })?,
4157 digest: None,
4158 }))),
4159 digest: None,
4160 })));
4161
4162 let (sketch_block_ref, _) = self.mutate_ast(
4164 new_ast,
4165 sketch_id,
4166 AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
4167 )?;
4168 Ok(sketch_block_ref)
4169 }
4170
4171 async fn add_vertical_distance(
4172 &mut self,
4173 sketch: ObjectId,
4174 distance: Distance,
4175 new_ast: &mut ast::Node<ast::Program>,
4176 ) -> Result<AstNodeRef, KclError> {
4177 let sketch_id = sketch;
4178 let [pt0_ast, pt1_ast] = match distance.points.as_slice() {
4179 [pt0, pt1] => [
4180 self.coincident_segment_to_ast(pt0, new_ast)?,
4181 self.coincident_segment_to_ast(pt1, new_ast)?,
4182 ],
4183 _ => {
4184 return Err(KclError::refactor(format!(
4185 "Vertical distance constraint must have exactly 2 points, got {}",
4186 distance.points.len()
4187 )));
4188 }
4189 };
4190
4191 let arguments = match &distance.label_position {
4192 Some(label_position) => vec![ast::LabeledArg {
4193 label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
4194 arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
4195 }],
4196 None => Default::default(),
4197 };
4198
4199 let distance_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
4201 callee: ast::Node::no_src(ast_sketch2_name(VERTICAL_DISTANCE_FN)),
4202 unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
4203 ast::ArrayExpression {
4204 elements: vec![pt0_ast, pt1_ast],
4205 digest: None,
4206 non_code_meta: Default::default(),
4207 },
4208 )))),
4209 arguments,
4210 digest: None,
4211 non_code_meta: Default::default(),
4212 })));
4213 let distance_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
4214 left: distance_call_ast,
4215 operator: ast::BinaryOperator::Eq,
4216 right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
4217 value: ast::LiteralValue::Number {
4218 value: distance.distance.value,
4219 suffix: distance.distance.units,
4220 },
4221 raw: format_number_literal(distance.distance.value, distance.distance.units, None).map_err(|_| {
4222 KclError::refactor(format!(
4223 "Could not format numeric suffix: {:?}",
4224 distance.distance.units
4225 ))
4226 })?,
4227 digest: None,
4228 }))),
4229 digest: None,
4230 })));
4231
4232 let (sketch_block_ref, _) = self.mutate_ast(
4234 new_ast,
4235 sketch_id,
4236 AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
4237 )?;
4238 Ok(sketch_block_ref)
4239 }
4240
4241 async fn add_horizontal(
4242 &mut self,
4243 sketch: ObjectId,
4244 horizontal: Horizontal,
4245 new_ast: &mut ast::Node<ast::Program>,
4246 ) -> Result<AstNodeRef, KclError> {
4247 let sketch_id = sketch;
4248
4249 let first_arg_ast = match horizontal {
4251 Horizontal::Line { line } => {
4252 let line_object = self
4253 .scene_graph
4254 .objects
4255 .get(line.0)
4256 .ok_or_else(|| KclError::refactor(format!("Line not found: {line:?}")))?;
4257 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4258 let kind = line_object.kind.human_friendly_kind_with_article();
4259 return Err(KclError::refactor(format!(
4260 "This constraint only works on Segments, but you selected {kind}"
4261 )));
4262 };
4263 let Segment::Line(_) = line_segment else {
4264 return Err(KclError::refactor(format!(
4265 "Only lines can be made horizontal, but you selected {}",
4266 line_segment.human_friendly_kind_with_article(),
4267 )));
4268 };
4269 self.line_id_to_ast_reference(line, new_ast)?
4270 }
4271 Horizontal::Points { points } => {
4272 let point_asts = points
4273 .iter()
4274 .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
4275 .collect::<Result<Vec<_>, _>>()?;
4276 ast::ArrayExpression::new(point_asts).into()
4277 }
4278 };
4279 let horizontal_ast = create_horizontal_ast(first_arg_ast);
4281
4282 let (sketch_block_ref, _) = self.mutate_ast(
4284 new_ast,
4285 sketch_id,
4286 AstMutateCommand::AddSketchBlockExprStmt { expr: horizontal_ast },
4287 )?;
4288 Ok(sketch_block_ref)
4289 }
4290
4291 async fn add_lines_equal_length(
4292 &mut self,
4293 sketch: ObjectId,
4294 lines_equal_length: LinesEqualLength,
4295 new_ast: &mut ast::Node<ast::Program>,
4296 ) -> Result<AstNodeRef, KclError> {
4297 if lines_equal_length.lines.len() < 2 {
4298 return Err(KclError::refactor(format!(
4299 "Lines equal length constraint must have at least 2 lines, got {}",
4300 lines_equal_length.lines.len()
4301 )));
4302 };
4303
4304 let sketch_id = sketch;
4305
4306 let line_asts = lines_equal_length
4308 .lines
4309 .iter()
4310 .map(|line_id| {
4311 let line_object = self
4312 .scene_graph
4313 .objects
4314 .get(line_id.0)
4315 .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
4316 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4317 let kind = line_object.kind.human_friendly_kind_with_article();
4318 return Err(KclError::refactor(format!(
4319 "This constraint only works on Segments, but you selected {kind}"
4320 )));
4321 };
4322 let Segment::Line(_) = line_segment else {
4323 let kind = line_segment.human_friendly_kind_with_article();
4324 return Err(KclError::refactor(format!(
4325 "Only lines can be made equal length, but you selected {kind}"
4326 )));
4327 };
4328
4329 self.line_id_to_ast_reference(*line_id, new_ast)
4330 })
4331 .collect::<Result<Vec<_>, _>>()?;
4332
4333 let equal_length_ast = create_equal_length_ast(line_asts);
4335
4336 let (sketch_block_ref, _) = self.mutate_ast(
4338 new_ast,
4339 sketch_id,
4340 AstMutateCommand::AddSketchBlockExprStmt { expr: equal_length_ast },
4341 )?;
4342 Ok(sketch_block_ref)
4343 }
4344
4345 fn equal_radius_segment_id_to_ast_reference(
4346 &mut self,
4347 segment_id: ObjectId,
4348 new_ast: &mut ast::Node<ast::Program>,
4349 ) -> Result<ast::Expr, KclError> {
4350 let segment_object = self
4351 .scene_graph
4352 .objects
4353 .get(segment_id.0)
4354 .ok_or_else(|| KclError::refactor(format!("Segment not found: {segment_id:?}")))?;
4355 let ObjectKind::Segment { segment } = &segment_object.kind else {
4356 return Err(KclError::refactor(format!(
4357 "Object is not a segment, it was {}",
4358 segment_object.kind.human_friendly_kind_with_article()
4359 )));
4360 };
4361
4362 let ref_type = match segment {
4363 Segment::Arc(_) => ARC_VARIABLE,
4364 Segment::Circle(_) => CIRCLE_VARIABLE,
4365 _ => {
4366 return Err(KclError::refactor(format!(
4367 "equalRadius supports only arc/circle segments, got {}",
4368 segment.human_friendly_kind_with_article()
4369 )));
4370 }
4371 };
4372
4373 get_or_insert_ast_reference(new_ast, &segment_object.source, ref_type, None)
4374 }
4375
4376 fn symmetric_input_id_to_ast_reference(
4377 &mut self,
4378 segment_id: ObjectId,
4379 new_ast: &mut ast::Node<ast::Program>,
4380 ) -> Result<ast::Expr, KclError> {
4381 let segment_object = self
4382 .scene_graph
4383 .objects
4384 .get(segment_id.0)
4385 .ok_or_else(|| KclError::refactor(format!("Segment not found: {segment_id:?}")))?;
4386 let ObjectKind::Segment { segment } = &segment_object.kind else {
4387 return Err(KclError::refactor(format!(
4388 "Object is not a segment, it was {}",
4389 segment_object.kind.human_friendly_kind_with_article()
4390 )));
4391 };
4392
4393 match segment {
4394 Segment::Point(_) => self.point_id_to_ast_reference(segment_id, new_ast),
4395 Segment::Line(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, LINE_VARIABLE, None),
4396 Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, ARC_VARIABLE, None),
4397 Segment::Circle(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, CIRCLE_VARIABLE, None),
4398 Segment::ControlPointSpline(_) => Err(KclError::refactor(
4399 "Symmetric does not yet support control point splines".to_owned(),
4400 )),
4401 }
4402 }
4403
4404 fn symmetric_axis_id_to_ast_reference(
4405 &mut self,
4406 segment_id: ObjectId,
4407 new_ast: &mut ast::Node<ast::Program>,
4408 ) -> Result<ast::Expr, KclError> {
4409 let segment_object = self
4410 .scene_graph
4411 .objects
4412 .get(segment_id.0)
4413 .ok_or_else(|| KclError::refactor(format!("Axis segment not found: {segment_id:?}")))?;
4414 let ObjectKind::Segment { segment } = &segment_object.kind else {
4415 return Err(KclError::refactor(format!(
4416 "Object is not a segment, it was {}",
4417 segment_object.kind.human_friendly_kind_with_article()
4418 )));
4419 };
4420 match segment {
4421 Segment::Line(_) => self.line_id_to_ast_reference(segment_id, new_ast),
4422 _ => Err(KclError::refactor(format!(
4423 "Symmetric axis must be a line, got {}",
4424 segment.human_friendly_kind_with_article()
4425 ))),
4426 }
4427 }
4428
4429 async fn add_parallel(
4430 &mut self,
4431 sketch: ObjectId,
4432 parallel: Parallel,
4433 new_ast: &mut ast::Node<ast::Program>,
4434 ) -> Result<AstNodeRef, KclError> {
4435 if parallel.lines.len() < 2 {
4436 return Err(KclError::refactor(format!(
4437 "Parallel constraint must have at least 2 lines, got {}",
4438 parallel.lines.len()
4439 )));
4440 };
4441
4442 let sketch_id = sketch;
4443
4444 let line_asts = parallel
4445 .lines
4446 .iter()
4447 .map(|line_id| {
4448 let line_object = self
4449 .scene_graph
4450 .objects
4451 .get(line_id.0)
4452 .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
4453 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4454 let kind = line_object.kind.human_friendly_kind_with_article();
4455 return Err(KclError::refactor(format!(
4456 "This constraint only works on Segments, but you selected {kind}"
4457 )));
4458 };
4459 let Segment::Line(_) = line_segment else {
4460 let kind = line_segment.human_friendly_kind_with_article();
4461 return Err(KclError::refactor(format!(
4462 "Only lines can be made parallel, but you selected {kind}"
4463 )));
4464 };
4465
4466 self.line_id_to_ast_reference(*line_id, new_ast)
4467 })
4468 .collect::<Result<Vec<_>, _>>()?;
4469
4470 let call_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
4471 callee: ast::Node::no_src(ast_sketch2_name(LinesAtAngleKind::Parallel.to_function_name())),
4472 unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
4473 ast::ArrayExpression {
4474 elements: line_asts,
4475 digest: None,
4476 non_code_meta: Default::default(),
4477 },
4478 )))),
4479 arguments: Default::default(),
4480 digest: None,
4481 non_code_meta: Default::default(),
4482 })));
4483
4484 let (sketch_block_ref, _) = self.mutate_ast(
4485 new_ast,
4486 sketch_id,
4487 AstMutateCommand::AddSketchBlockExprStmt { expr: call_ast },
4488 )?;
4489 Ok(sketch_block_ref)
4490 }
4491
4492 async fn add_perpendicular(
4493 &mut self,
4494 sketch: ObjectId,
4495 perpendicular: Perpendicular,
4496 new_ast: &mut ast::Node<ast::Program>,
4497 ) -> Result<AstNodeRef, KclError> {
4498 self.add_lines_at_angle_constraint(sketch, LinesAtAngleKind::Perpendicular, perpendicular.lines, new_ast)
4499 .await
4500 }
4501
4502 async fn add_lines_at_angle_constraint(
4503 &mut self,
4504 sketch: ObjectId,
4505 angle_kind: LinesAtAngleKind,
4506 lines: Vec<ObjectId>,
4507 new_ast: &mut ast::Node<ast::Program>,
4508 ) -> Result<AstNodeRef, KclError> {
4509 let &[line0_id, line1_id] = lines.as_slice() else {
4510 return Err(KclError::refactor(format!(
4511 "{} constraint must have exactly 2 lines, got {}",
4512 angle_kind.to_function_name(),
4513 lines.len()
4514 )));
4515 };
4516
4517 let sketch_id = sketch;
4518
4519 let line0_object = self
4521 .scene_graph
4522 .objects
4523 .get(line0_id.0)
4524 .ok_or_else(|| KclError::refactor(format!("Line not found: {line0_id:?}")))?;
4525 let ObjectKind::Segment { segment: line0_segment } = &line0_object.kind else {
4526 let kind = line0_object.kind.human_friendly_kind_with_article();
4527 return Err(KclError::refactor(format!(
4528 "This constraint only works on Segments, but you selected {kind}"
4529 )));
4530 };
4531 let Segment::Line(_) = line0_segment else {
4532 return Err(KclError::refactor(format!(
4533 "Only lines can be made {}, but you selected {}",
4534 angle_kind.to_function_name(),
4535 line0_segment.human_friendly_kind_with_article(),
4536 )));
4537 };
4538 let line0_ast = self.line_id_to_ast_reference(line0_id, new_ast)?;
4539
4540 let line1_object = self
4541 .scene_graph
4542 .objects
4543 .get(line1_id.0)
4544 .ok_or_else(|| KclError::refactor(format!("Line not found: {line1_id:?}")))?;
4545 let ObjectKind::Segment { segment: line1_segment } = &line1_object.kind else {
4546 let kind = line1_object.kind.human_friendly_kind_with_article();
4547 return Err(KclError::refactor(format!(
4548 "This constraint only works on Segments, but you selected {kind}"
4549 )));
4550 };
4551 let Segment::Line(_) = line1_segment else {
4552 return Err(KclError::refactor(format!(
4553 "Only lines can be made {}, but you selected {}",
4554 angle_kind.to_function_name(),
4555 line1_segment.human_friendly_kind_with_article(),
4556 )));
4557 };
4558 let line1_ast = self.line_id_to_ast_reference(line1_id, new_ast)?;
4559
4560 let call_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
4562 callee: ast::Node::no_src(ast_sketch2_name(angle_kind.to_function_name())),
4563 unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
4564 ast::ArrayExpression {
4565 elements: vec![line0_ast, line1_ast],
4566 digest: None,
4567 non_code_meta: Default::default(),
4568 },
4569 )))),
4570 arguments: Default::default(),
4571 digest: None,
4572 non_code_meta: Default::default(),
4573 })));
4574
4575 let (sketch_block_ref, _) = self.mutate_ast(
4577 new_ast,
4578 sketch_id,
4579 AstMutateCommand::AddSketchBlockExprStmt { expr: call_ast },
4580 )?;
4581 Ok(sketch_block_ref)
4582 }
4583
4584 async fn add_vertical(
4585 &mut self,
4586 sketch: ObjectId,
4587 vertical: Vertical,
4588 new_ast: &mut ast::Node<ast::Program>,
4589 ) -> Result<AstNodeRef, KclError> {
4590 let sketch_id = sketch;
4591
4592 let first_arg_ast = match vertical {
4593 Vertical::Line { line } => {
4594 let line_object = self
4596 .scene_graph
4597 .objects
4598 .get(line.0)
4599 .ok_or_else(|| KclError::refactor(format!("Line not found: {line:?}")))?;
4600 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4601 let kind = line_object.kind.human_friendly_kind_with_article();
4602 return Err(KclError::refactor(format!(
4603 "This constraint only works on Segments, but you selected {kind}"
4604 )));
4605 };
4606 let Segment::Line(_) = line_segment else {
4607 return Err(KclError::refactor(format!(
4608 "Only lines can be made vertical, but you selected {}",
4609 line_segment.human_friendly_kind_with_article()
4610 )));
4611 };
4612 self.line_id_to_ast_reference(line, new_ast)?
4613 }
4614 Vertical::Points { points } => {
4615 let point_asts = points
4616 .iter()
4617 .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
4618 .collect::<Result<Vec<_>, _>>()?;
4619 ast::ArrayExpression::new(point_asts).into()
4620 }
4621 };
4622 let vertical_ast = create_vertical_ast(first_arg_ast);
4624
4625 let (sketch_block_ref, _) = self.mutate_ast(
4627 new_ast,
4628 sketch_id,
4629 AstMutateCommand::AddSketchBlockExprStmt { expr: vertical_ast },
4630 )?;
4631 Ok(sketch_block_ref)
4632 }
4633
4634 async fn execute_after_add_constraint(
4635 &mut self,
4636 ctx: &ExecutorContext,
4637 sketch_id: ObjectId,
4638 sketch_block_ref: AstNodeRef,
4639 new_ast: &mut ast::Node<ast::Program>,
4640 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
4641 let new_source = source_from_ast(new_ast);
4643 let (new_program, errors) = Program::parse(&new_source)
4645 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
4646 if !errors.is_empty() {
4647 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
4648 "Error parsing KCL source after adding constraint: {errors:?}"
4649 ))));
4650 }
4651 let Some(new_program) = new_program else {
4652 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
4653 "No AST produced after adding constraint".to_string(),
4654 )));
4655 };
4656 let constraint_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
4657 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
4658 "Source range of new constraint not found in sketch block: {sketch_block_ref:?}; {err:?}"
4659 )))
4660 })?;
4661
4662 let mut truncated_program = new_program.clone();
4665 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
4666 .map_err(KclErrorWithOutputs::no_outputs)?;
4667
4668 let outcome = ctx
4670 .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch_id))
4671 .await?;
4672
4673 let new_object_ids = {
4674 let constraint_id = outcome
4676 .source_range_to_object
4677 .get(&constraint_node_ref.range)
4678 .copied()
4679 .ok_or_else(|| {
4680 KclErrorWithOutputs::from_error_outcome(
4681 KclError::refactor(format!("Source range of constraint not found: {constraint_node_ref:?}")),
4682 outcome.clone(),
4683 )
4684 })?;
4685 vec![constraint_id]
4686 };
4687
4688 self.program = new_program;
4691
4692 let outcome = self.update_state_after_exec(outcome, true);
4694
4695 let src_delta = self.commit_var_solutions_to_program(&outcome, "adding constraint")?;
4696 let scene_graph_delta = SceneGraphDelta {
4697 new_graph: self.scene_graph_for_ui(),
4698 invalidates_ids: false,
4699 new_objects: new_object_ids,
4700 exec_outcome: outcome,
4701 };
4702 Ok((src_delta, scene_graph_delta))
4703 }
4704
4705 fn commit_var_solutions_to_program(&mut self, outcome: &ExecOutcome, operation: &str) -> ExecResult<SourceDelta> {
4706 let commit_failure = || {
4707 KclErrorWithOutputs::from_error_outcome(
4708 KclError::refactor(format!("Could not update KCL after {operation}.")),
4709 outcome.clone(),
4710 )
4711 };
4712
4713 let default_length_unit = self.default_length_unit();
4714 let mut settled_ast = self.program.ast.clone();
4715 let mut committed_solver_value = false;
4716 for (var_range, node_path, value) in &outcome.var_solutions {
4717 let Some(lookup) = numeric_literal_at_node_path(&settled_ast, node_path.as_ref(), *var_range) else {
4718 return Err(commit_failure());
4719 };
4720 let new_value = match &lookup {
4721 Some(current_literal) => {
4722 if !var_solution_needs_commit(current_literal, *value, default_length_unit) {
4723 continue;
4724 }
4725 preserve_var_solution_literal_style(current_literal, *value, default_length_unit)
4726 }
4727 None => {
4728 Number {
4732 value: number_value_in_default_length_units(*value, default_length_unit),
4733 units: default_length_unit.into(),
4734 }
4735 }
4736 };
4737 committed_solver_value = true;
4738 let source_ref = SourceRef::Simple {
4739 range: *var_range,
4740 node_path: node_path.clone(),
4741 };
4742 mutate_ast_node_by_source_ref(
4743 &mut settled_ast,
4744 &source_ref,
4745 AstMutateCommand::EditVarInitialValue { value: new_value },
4746 )
4747 .map_err(|_| commit_failure())?;
4748 }
4749
4750 if !committed_solver_value {
4751 return Ok(SourceDelta {
4752 text: self.program.original_file_contents.clone(),
4753 });
4754 }
4755
4756 let settled_source = source_from_ast(&settled_ast);
4757 let (settled_program, errors) = Program::parse(&settled_source).map_err(|_| commit_failure())?;
4758 if !errors.is_empty() {
4759 return Err(commit_failure());
4760 }
4761 let Some(settled_program) = settled_program else {
4762 return Err(commit_failure());
4763 };
4764
4765 self.program = settled_program;
4766
4767 Ok(SourceDelta { text: settled_source })
4768 }
4769
4770 fn segment_will_be_deleted(&self, segment_id: ObjectId, segment_ids_set: &AhashIndexSet<ObjectId>) -> bool {
4772 if segment_ids_set.contains(&segment_id) {
4773 return true;
4774 }
4775
4776 let Some(segment_object) = self.scene_graph.objects.get(segment_id.0) else {
4777 return false;
4778 };
4779 let ObjectKind::Segment { segment } = &segment_object.kind else {
4780 return false;
4781 };
4782 let Segment::Point(point) = segment else {
4783 return false;
4784 };
4785
4786 point.owner.is_some_and(|owner_id| segment_ids_set.contains(&owner_id))
4787 }
4788
4789 fn remaining_constraint_segments(
4790 &self,
4791 segments: &[ConstraintSegment],
4792 segment_ids_set: &AhashIndexSet<ObjectId>,
4793 ) -> Vec<ConstraintSegment> {
4794 segments
4795 .iter()
4796 .copied()
4797 .filter(|segment| match segment {
4798 ConstraintSegment::Origin(_) => true,
4799 ConstraintSegment::Segment(segment_id) => !self.segment_will_be_deleted(*segment_id, segment_ids_set),
4800 })
4801 .collect()
4802 }
4803
4804 fn find_referenced_constraints(
4805 &self,
4806 sketch_id: ObjectId,
4807 segment_ids_set: &AhashIndexSet<ObjectId>,
4808 ) -> Result<AhashIndexSet<ObjectId>, KclError> {
4809 let sketch_object = self
4811 .scene_graph
4812 .objects
4813 .get(sketch_id.0)
4814 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch_id:?}")))?;
4815 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
4816 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
4817 };
4818 let segment_or_owner_matches = |segment_id: ObjectId| {
4819 if segment_ids_set.contains(&segment_id) {
4820 return true;
4821 }
4822 let segment_object = self.scene_graph.objects.get(segment_id.0);
4823 if let Some(obj) = segment_object
4824 && let ObjectKind::Segment { segment } = &obj.kind
4825 {
4826 match segment {
4827 Segment::Point(point) => point.owner.is_some_and(|owner_id| segment_ids_set.contains(&owner_id)),
4828 Segment::Line(line) => line.owner.is_some_and(|owner_id| segment_ids_set.contains(&owner_id)),
4829 _ => false,
4830 }
4831 } else {
4832 false
4833 }
4834 };
4835 let mut constraint_ids_set = AhashIndexSet::default();
4836 for constraint_id in &sketch.constraints {
4837 let constraint_object = self
4838 .scene_graph
4839 .objects
4840 .get(constraint_id.0)
4841 .ok_or_else(|| KclError::refactor(format!("Constraint not found: {constraint_id:?}")))?;
4842 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
4843 return Err(KclError::refactor(format!(
4844 "Object is not a constraint, it is {}",
4845 constraint_object.kind.human_friendly_kind_with_article()
4846 )));
4847 };
4848 let depends_on_segment = match constraint {
4849 Constraint::Coincident(c) => c.segment_ids().any(segment_or_owner_matches),
4850 Constraint::Distance(d) => d.point_ids().any(segment_or_owner_matches),
4851 Constraint::Fixed(fixed) => fixed
4852 .points
4853 .iter()
4854 .any(|fixed_point| self.segment_will_be_deleted(fixed_point.point, segment_ids_set)),
4855 Constraint::Radius(r) => segment_or_owner_matches(r.arc),
4856 Constraint::Diameter(d) => segment_or_owner_matches(d.arc),
4857 Constraint::EqualRadius(equal_radius) => {
4858 equal_radius.input.iter().copied().any(segment_or_owner_matches)
4859 }
4860 Constraint::HorizontalDistance(d) => d.point_ids().any(segment_or_owner_matches),
4861 Constraint::VerticalDistance(d) => d.point_ids().any(segment_or_owner_matches),
4862 Constraint::Horizontal(h) => match h {
4863 Horizontal::Line { line } => segment_or_owner_matches(*line),
4864 Horizontal::Points { points } => points.iter().any(|point| match point {
4865 ConstraintSegment::Segment(point) => segment_or_owner_matches(*point),
4866 ConstraintSegment::Origin(_) => false,
4867 }),
4868 },
4869 Constraint::Vertical(v) => match v {
4870 Vertical::Line { line } => segment_or_owner_matches(*line),
4871 Vertical::Points { points } => points.iter().any(|point| match point {
4872 ConstraintSegment::Segment(point) => segment_or_owner_matches(*point),
4873 ConstraintSegment::Origin(_) => false,
4874 }),
4875 },
4876 Constraint::LinesEqualLength(lines_equal_length) => {
4877 lines_equal_length.lines.iter().copied().any(segment_or_owner_matches)
4878 }
4879 Constraint::Midpoint(midpoint) => {
4880 segment_or_owner_matches(midpoint.segment)
4881 || matches!(
4882 midpoint.point,
4883 ConstraintSegment::Segment(point) if segment_or_owner_matches(point)
4884 )
4885 }
4886 Constraint::Parallel(parallel) => parallel.lines.iter().copied().any(segment_or_owner_matches),
4887 Constraint::Perpendicular(perpendicular) => {
4888 perpendicular.lines.iter().copied().any(segment_or_owner_matches)
4889 }
4890 Constraint::Angle(angle) => angle.lines.iter().copied().any(segment_or_owner_matches),
4891 Constraint::Symmetric(symmetric) => {
4892 segment_or_owner_matches(symmetric.axis)
4893 || symmetric.input.iter().copied().any(segment_or_owner_matches)
4894 }
4895 Constraint::Tangent(tangent) => tangent.input.iter().copied().any(segment_or_owner_matches),
4896 };
4897 if depends_on_segment {
4898 constraint_ids_set.insert(*constraint_id);
4899 }
4900 }
4901 Ok(constraint_ids_set)
4902 }
4903
4904 fn update_state_after_exec(&mut self, outcome: ExecOutcome, freedom_analysis_ran: bool) -> ExecOutcome {
4905 let mut outcome = outcome;
4906 let mut new_objects = std::mem::take(&mut outcome.scene_objects);
4907
4908 if freedom_analysis_ran {
4909 self.point_freedom_cache.clear();
4912 for new_obj in &new_objects {
4913 if let ObjectKind::Segment {
4914 segment: crate::front::Segment::Point(point),
4915 } = &new_obj.kind
4916 {
4917 self.point_freedom_cache.insert(new_obj.id, point.freedom);
4918 }
4919 }
4920 add_wall_and_cap_face_objects(&mut new_objects, &outcome.artifact_graph);
4921 self.scene_graph.objects = new_objects;
4923 } else {
4924 for old_obj in &self.scene_graph.objects {
4927 if let ObjectKind::Segment {
4928 segment: crate::front::Segment::Point(point),
4929 } = &old_obj.kind
4930 {
4931 self.point_freedom_cache.insert(old_obj.id, point.freedom);
4932 }
4933 }
4934
4935 let mut updated_objects = Vec::with_capacity(new_objects.len());
4937 for new_obj in new_objects {
4938 let mut obj = new_obj;
4939 if let ObjectKind::Segment {
4940 segment: crate::front::Segment::Point(point),
4941 } = &mut obj.kind
4942 {
4943 let new_freedom = point.freedom;
4944 match new_freedom {
4950 Freedom::Free => {
4951 match self.point_freedom_cache.get(&obj.id).copied() {
4952 Some(Freedom::Conflict) => {
4953 }
4956 Some(Freedom::Fixed) => {
4957 point.freedom = Freedom::Fixed;
4959 }
4960 Some(Freedom::Free) => {
4961 }
4963 None => {
4964 }
4966 }
4967 }
4968 Freedom::Fixed => {
4969 }
4971 Freedom::Conflict => {
4972 }
4974 }
4975 self.point_freedom_cache.insert(obj.id, point.freedom);
4977 }
4978 updated_objects.push(obj);
4979 }
4980
4981 add_wall_and_cap_face_objects(&mut updated_objects, &outcome.artifact_graph);
4982 self.scene_graph.objects = updated_objects;
4983 }
4984 outcome
4985 }
4986
4987 fn mutate_ast(
4988 &mut self,
4989 ast: &mut ast::Node<ast::Program>,
4990 object_id: ObjectId,
4991 command: AstMutateCommand,
4992 ) -> Result<(AstNodeRef, AstMutateCommandReturn), KclError> {
4993 let sketch_object = self
4994 .scene_graph
4995 .objects
4996 .get(object_id.0)
4997 .ok_or_else(|| KclError::refactor(format!("Object not found: {object_id:?}")))?;
4998 mutate_ast_node_by_source_ref(ast, &sketch_object.source, command)
4999 }
5000}
5001
5002fn sketch_block_ref_from_id(scene_graph: &SceneGraph, sketch_id: ObjectId) -> Result<AstNodeRef, KclError> {
5003 let sketch_object = scene_graph
5005 .objects
5006 .get(sketch_id.0)
5007 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch_id:?}")))?;
5008 let ObjectKind::Sketch(_) = &sketch_object.kind else {
5009 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
5010 };
5011 expect_single_node_ref(sketch_object)
5012}
5013
5014fn expect_single_node_ref(object: &Object) -> Result<AstNodeRef, KclError> {
5015 match &object.source {
5016 SourceRef::Simple { range, node_path } => Ok(AstNodeRef {
5017 range: *range,
5018 node_path: node_path.clone(),
5019 }),
5020 SourceRef::BackTrace { ranges } => {
5021 let [range] = ranges.as_slice() else {
5022 return Err(KclError::refactor(format!(
5023 "Expected single location in SourceRef, got {}; ranges={ranges:#?}",
5024 ranges.len()
5025 )));
5026 };
5027 Ok(AstNodeRef {
5028 range: range.0,
5029 node_path: range.1.clone(),
5030 })
5031 }
5032 }
5033}
5034
5035fn only_sketch_block_from_range(
5038 ast: &mut ast::Node<ast::Program>,
5039 sketch_block_range: SourceRange,
5040 edit_kind: ChangeKind,
5041) -> Result<(), KclError> {
5042 let r1 = sketch_block_range;
5043 let matches_range = |r2: SourceRange| -> bool {
5044 match edit_kind {
5047 ChangeKind::Add => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() <= r2.end(),
5048 ChangeKind::Edit => r1.module_id() == r2.module_id() && r1.start() == r2.start(),
5050 ChangeKind::Delete => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() >= r2.end(),
5051 ChangeKind::None => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() == r2.end(),
5053 }
5054 };
5055 let mut found = false;
5056 for item in ast.body.iter_mut() {
5057 match item {
5058 ast::BodyItem::ImportStatement(_) => {}
5059 ast::BodyItem::ExpressionStatement(node) => {
5060 if matches_range(SourceRange::from(&*node))
5061 && let ast::Expr::SketchBlock(sketch_block) = &mut node.expression
5062 {
5063 sketch_block.is_being_edited = true;
5064 found = true;
5065 break;
5066 }
5067 }
5068 ast::BodyItem::VariableDeclaration(node) => {
5069 if matches_range(SourceRange::from(&node.declaration.init))
5070 && let ast::Expr::SketchBlock(sketch_block) = &mut node.declaration.init
5071 {
5072 sketch_block.is_being_edited = true;
5073 found = true;
5074 break;
5075 }
5076 }
5077 ast::BodyItem::TypeDeclaration(_) => {}
5078 ast::BodyItem::ReturnStatement(node) => {
5079 if matches_range(SourceRange::from(&node.argument))
5080 && let ast::Expr::SketchBlock(sketch_block) = &mut node.argument
5081 {
5082 sketch_block.is_being_edited = true;
5083 found = true;
5084 break;
5085 }
5086 }
5087 }
5088 }
5089 if !found {
5090 return Err(KclError::refactor(format!(
5091 "Sketch block source range not found in AST: {sketch_block_range:?}, edit_kind={edit_kind:?}"
5092 )));
5093 }
5094
5095 Ok(())
5096}
5097
5098fn only_sketch_block(
5099 ast: &mut ast::Node<ast::Program>,
5100 sketch_block_ref: &AstNodeRef,
5101 edit_kind: ChangeKind,
5102) -> Result<(), KclError> {
5103 let Some(target_node_path) = &sketch_block_ref.node_path else {
5104 #[cfg(target_arch = "wasm32")]
5105 web_sys::console::warn_1(
5106 &format!(
5107 "only_sketch_block: target sketch block ref doesn't have node path; sketch_block_ref={:#?}, edit_kind={edit_kind:#?}",
5108 &sketch_block_ref
5109 )
5110 .into(),
5111 );
5112 return only_sketch_block_from_range(ast, sketch_block_ref.range, edit_kind);
5113 };
5114 let mut found = false;
5115 for item in ast.body.iter_mut() {
5116 match item {
5117 ast::BodyItem::ImportStatement(_) => {}
5118 ast::BodyItem::ExpressionStatement(node) => {
5119 if let Some(node_path) = &node.node_path
5121 && node_path == target_node_path
5122 && let ast::Expr::SketchBlock(sketch_block) = &mut node.expression
5123 {
5124 sketch_block.is_being_edited = true;
5125 found = true;
5126 break;
5127 }
5128 if let Some(node_path) = node.expression.node_path()
5130 && node_path == target_node_path
5131 && let ast::Expr::SketchBlock(sketch_block) = &mut node.expression
5132 {
5133 sketch_block.is_being_edited = true;
5134 found = true;
5135 break;
5136 }
5137 }
5138 ast::BodyItem::VariableDeclaration(node) => {
5139 if let Some(node_path) = node.declaration.init.node_path()
5140 && node_path == target_node_path
5141 && let ast::Expr::SketchBlock(sketch_block) = &mut node.declaration.init
5142 {
5143 sketch_block.is_being_edited = true;
5144 found = true;
5145 break;
5146 }
5147 }
5148 ast::BodyItem::TypeDeclaration(_) => {}
5149 ast::BodyItem::ReturnStatement(node) => {
5150 if let Some(node_path) = node.argument.node_path()
5151 && node_path == target_node_path
5152 && let ast::Expr::SketchBlock(sketch_block) = &mut node.argument
5153 {
5154 sketch_block.is_being_edited = true;
5155 found = true;
5156 break;
5157 }
5158 }
5159 }
5160 }
5161 if !found {
5162 return Err(KclError::refactor(format!(
5163 "Sketch block node path not found in AST: {sketch_block_ref:?}, edit_kind={edit_kind:?}"
5164 )));
5165 }
5166
5167 Ok(())
5168}
5169
5170fn sketch_on_ast_expr(
5171 ast: &mut ast::Node<ast::Program>,
5172 scene_graph: &SceneGraph,
5173 on: &Plane,
5174) -> Result<ast::Expr, KclError> {
5175 match on {
5176 Plane::Default(name) => Ok(default_plane_ast_expr(*name)),
5177 Plane::Object(object_id) => {
5178 let on_object = scene_graph
5179 .objects
5180 .get(object_id.0)
5181 .ok_or_else(|| KclError::refactor(format!("Sketch plane object not found: {object_id:?}")))?;
5182 if let Some(face_expr) = sketch_face_of_scene_object_ast_expr(ast, on_object)? {
5183 return Ok(face_expr);
5184 }
5185 get_or_insert_ast_reference(ast, &on_object.source, "plane", None)
5186 }
5187 }
5188}
5189
5190fn sketch_face_of_scene_object_ast_expr(
5191 ast: &mut ast::Node<ast::Program>,
5192 on_object: &crate::front::Object,
5193) -> Result<Option<ast::Expr>, KclError> {
5194 let SourceRef::BackTrace { ranges } = &on_object.source else {
5195 return Ok(None);
5196 };
5197
5198 match &on_object.kind {
5199 ObjectKind::Wall(_) => {
5200 let (solid_range, sweep_range, segment_range) = match ranges.as_slice() {
5201 [sweep_range, segment_range] => (sweep_range, sweep_range, segment_range),
5202 [solid_range, sweep_range, segment_range] => (solid_range, sweep_range, segment_range),
5203 _ => {
5204 return Err(KclError::refactor(format!(
5205 "Expected wall source metadata to have 2 or 3 ranges, got {}; artifact_id={:?}",
5206 ranges.len(),
5207 on_object.artifact_id
5208 )));
5209 }
5210 };
5211 let solid_ref = get_or_insert_ast_reference(
5212 ast,
5213 &SourceRef::Simple {
5214 range: solid_range.0,
5215 node_path: solid_range.1.clone(),
5216 },
5217 "solid",
5218 None,
5219 )?;
5220 let ast::Expr::Name(solid_name_expr) = solid_ref else {
5221 return Err(KclError::refactor(format!(
5222 "Could not resolve solid reference for selected wall: artifact_id={:?}",
5223 on_object.artifact_id
5224 )));
5225 };
5226 let solid_expr = ast_name_expr(solid_name_expr.name.name.clone());
5227 let sweep_ref = get_or_insert_ast_reference(
5228 ast,
5229 &SourceRef::Simple {
5230 range: sweep_range.0,
5231 node_path: sweep_range.1.clone(),
5232 },
5233 "solid",
5234 None,
5235 )?;
5236 let ast::Expr::Name(sweep_name_expr) = sweep_ref else {
5237 return Err(KclError::refactor(format!(
5238 "Could not resolve sweep reference for selected wall: artifact_id={:?}",
5239 on_object.artifact_id
5240 )));
5241 };
5242 let sweep_name = sweep_name_expr.name.name.clone();
5243 let segment_ref = get_or_insert_ast_reference(
5244 ast,
5245 &SourceRef::Simple {
5246 range: segment_range.0,
5247 node_path: segment_range.1.clone(),
5248 },
5249 LINE_VARIABLE,
5250 None,
5251 )?;
5252
5253 let face_expr = if let Some(region_name) = region_name_from_sweep_variable(ast, &sweep_name) {
5254 let ast::Expr::Name(segment_name_expr) = segment_ref else {
5255 return Err(KclError::refactor(format!(
5256 "Could not resolve source segment reference for selected region wall: artifact_id={:?}",
5257 on_object.artifact_id
5258 )));
5259 };
5260 create_member_expression(
5261 create_member_expression(ast_name_expr(region_name), "tags"),
5262 &segment_name_expr.name.name,
5263 )
5264 } else {
5265 segment_ref
5266 };
5267
5268 Ok(Some(create_face_of_ast(solid_expr, face_expr)))
5269 }
5270 ObjectKind::Cap(cap) => {
5271 let solid_range = match ranges.as_slice() {
5272 [solid_range] | [solid_range, _] => solid_range,
5273 _ => {
5274 return Err(KclError::refactor(format!(
5275 "Expected cap source metadata to have 1 or 2 ranges, got {}; artifact_id={:?}",
5276 ranges.len(),
5277 on_object.artifact_id
5278 )));
5279 }
5280 };
5281 let solid_ref = get_or_insert_ast_reference(
5282 ast,
5283 &SourceRef::Simple {
5284 range: solid_range.0,
5285 node_path: solid_range.1.clone(),
5286 },
5287 "solid",
5288 None,
5289 )?;
5290 let ast::Expr::Name(solid_name_expr) = solid_ref else {
5291 return Err(KclError::refactor(format!(
5292 "Could not resolve solid reference for selected cap: artifact_id={:?}",
5293 on_object.artifact_id
5294 )));
5295 };
5296 let solid_expr = ast_name_expr(solid_name_expr.name.name.clone());
5297 let face_expr = match cap.kind {
5299 crate::frontend::api::CapKind::Start => ast_name_expr("START".to_owned()),
5300 crate::frontend::api::CapKind::End => ast_name_expr("END".to_owned()),
5301 };
5302
5303 Ok(Some(create_face_of_ast(solid_expr, face_expr)))
5304 }
5305 _ => Ok(None),
5306 }
5307}
5308
5309fn downstream_composite_code_ref_for_source(artifact_graph: &ArtifactGraph, source_id: ArtifactId) -> Option<&CodeRef> {
5310 let mut current_id = source_id;
5311 let mut current_composite = None;
5312 let mut visited = HashSet::new();
5313
5314 while visited.insert(current_id) {
5315 let next_composite_id = downstream_composite_id_for_solid_source(artifact_graph, current_id);
5316
5317 let Some(composite_id) = next_composite_id else {
5318 break;
5319 };
5320 let Some(Artifact::CompositeSolid(composite)) = artifact_graph.get(&composite_id) else {
5321 break;
5322 };
5323
5324 current_id = composite.id;
5325 current_composite = Some(composite);
5326
5327 if !composite.consumed {
5328 break;
5329 }
5330 }
5331
5332 current_composite.map(|composite| &composite.code_ref)
5333}
5334
5335fn downstream_composite_id_for_solid_source(
5336 artifact_graph: &ArtifactGraph,
5337 source_id: ArtifactId,
5338) -> Option<ArtifactId> {
5339 if let Some(Artifact::Path(path)) = artifact_graph.get(&source_id)
5341 && let Some(composite_id) = path.composite_solid_id
5342 && let Some(Artifact::CompositeSolid(composite)) = artifact_graph.get(&composite_id)
5343 && composite_contains_path_input(&composite.solid_ids, &composite.tool_ids, path.id, path.solid2d_id)
5344 {
5345 return Some(composite_id);
5346 }
5347
5348 for artifact in artifact_graph.values() {
5350 if let Artifact::Path(path) = artifact
5351 && path.sweep_id == Some(source_id)
5352 && let Some(composite_id) = path.composite_solid_id
5353 && let Some(Artifact::CompositeSolid(composite)) = artifact_graph.get(&composite_id)
5354 && composite_contains_path_input(&composite.solid_ids, &composite.tool_ids, path.id, path.solid2d_id)
5355 {
5356 return Some(composite_id);
5357 }
5358 }
5359
5360 artifact_graph.values().find_map(|artifact| {
5362 let Artifact::CompositeSolid(composite) = artifact else {
5363 return None;
5364 };
5365 composite_contains_input(&composite.solid_ids, &composite.tool_ids, source_id).then_some(composite.id)
5366 })
5367}
5368
5369fn composite_contains_path_input(
5370 solid_ids: &[ArtifactId],
5371 tool_ids: &[ArtifactId],
5372 path_id: ArtifactId,
5373 solid2d_id: Option<ArtifactId>,
5374) -> bool {
5375 composite_contains_input(solid_ids, tool_ids, path_id)
5376 || solid2d_id.is_some_and(|solid2d_id| composite_contains_input(solid_ids, tool_ids, solid2d_id))
5377}
5378
5379fn composite_contains_input(solid_ids: &[ArtifactId], tool_ids: &[ArtifactId], input_id: ArtifactId) -> bool {
5380 solid_ids.contains(&input_id) || tool_ids.contains(&input_id)
5381}
5382
5383fn code_ref_source_ref_range(code_ref: &CodeRef) -> (SourceRange, Option<crate::NodePath>) {
5384 let node_path = (!code_ref.node_path.is_empty()).then(|| code_ref.node_path.clone());
5385 (code_ref.range, node_path)
5386}
5387
5388fn add_wall_and_cap_face_objects(scene_objects: &mut Vec<crate::front::Object>, artifact_graph: &ArtifactGraph) {
5389 let mut existing_artifact_ids = scene_objects
5390 .iter()
5391 .map(|object| object.artifact_id)
5392 .collect::<HashSet<_>>();
5393
5394 for artifact in artifact_graph.values() {
5395 match artifact {
5396 Artifact::Wall(wall) => {
5397 if existing_artifact_ids.contains(&wall.id) {
5398 continue;
5399 }
5400
5401 let Some(segment) = artifact_graph.get(&wall.seg_id).and_then(|artifact| match artifact {
5402 Artifact::Segment(segment) => Some(segment),
5403 _ => None,
5404 }) else {
5405 continue;
5406 };
5407 let Some(sweep) = artifact_graph.get(&wall.sweep_id).and_then(|artifact| match artifact {
5408 Artifact::Sweep(sweep) => Some(sweep),
5409 _ => None,
5410 }) else {
5411 continue;
5412 };
5413 let source_segment = segment
5414 .original_seg_id
5415 .and_then(|original_seg_id| artifact_graph.get(&original_seg_id))
5416 .and_then(|artifact| match artifact {
5417 Artifact::Segment(segment) => Some(segment),
5418 _ => None,
5419 })
5420 .unwrap_or(segment);
5421 let solid_code_ref =
5422 downstream_composite_code_ref_for_source(artifact_graph, wall.sweep_id).unwrap_or(&sweep.code_ref);
5423 let mut ranges = Vec::new();
5424 if solid_code_ref.range != sweep.code_ref.range || solid_code_ref.node_path != sweep.code_ref.node_path
5425 {
5426 ranges.push(code_ref_source_ref_range(solid_code_ref));
5427 }
5428 ranges.push(code_ref_source_ref_range(&sweep.code_ref));
5429 ranges.push(code_ref_source_ref_range(&source_segment.code_ref));
5430 let id = ObjectId(scene_objects.len());
5431 scene_objects.push(crate::front::Object {
5432 id,
5433 kind: ObjectKind::Wall(crate::frontend::api::Wall { id }),
5434 label: Default::default(),
5435 comments: Default::default(),
5436 artifact_id: wall.id,
5437 source: SourceRef::BackTrace { ranges },
5438 });
5439 existing_artifact_ids.insert(wall.id);
5440 }
5441 Artifact::Cap(cap) => {
5442 if existing_artifact_ids.contains(&cap.id) {
5443 continue;
5444 }
5445
5446 let Some(sweep) = artifact_graph.get(&cap.sweep_id).and_then(|artifact| match artifact {
5447 Artifact::Sweep(sweep) => Some(sweep),
5448 _ => None,
5449 }) else {
5450 continue;
5451 };
5452 let id = ObjectId(scene_objects.len());
5453 let kind = match cap.sub_type {
5454 CapSubType::Start => crate::frontend::api::CapKind::Start,
5455 CapSubType::End => crate::frontend::api::CapKind::End,
5456 };
5457 let solid_code_ref =
5458 downstream_composite_code_ref_for_source(artifact_graph, cap.sweep_id).unwrap_or(&sweep.code_ref);
5459 let mut ranges = Vec::new();
5460 if solid_code_ref.range != sweep.code_ref.range || solid_code_ref.node_path != sweep.code_ref.node_path
5461 {
5462 ranges.push(code_ref_source_ref_range(solid_code_ref));
5463 }
5464 ranges.push(code_ref_source_ref_range(&sweep.code_ref));
5465 scene_objects.push(crate::front::Object {
5466 id,
5467 kind: ObjectKind::Cap(crate::frontend::api::Cap { id, kind }),
5468 label: Default::default(),
5469 comments: Default::default(),
5470 artifact_id: cap.id,
5471 source: SourceRef::BackTrace { ranges },
5472 });
5473 existing_artifact_ids.insert(cap.id);
5474 }
5475 _ => {}
5476 }
5477 }
5478}
5479
5480fn default_plane_ast_expr(name: crate::engine::PlaneName) -> ast::Expr {
5481 use crate::engine::PlaneName;
5482
5483 match name {
5484 PlaneName::Xy => ast_name_expr("XY".to_owned()),
5485 PlaneName::Xz => ast_name_expr("XZ".to_owned()),
5486 PlaneName::Yz => ast_name_expr("YZ".to_owned()),
5487 PlaneName::NegXy => negated_plane_ast_expr("XY"),
5488 PlaneName::NegXz => negated_plane_ast_expr("XZ"),
5489 PlaneName::NegYz => negated_plane_ast_expr("YZ"),
5490 }
5491}
5492
5493fn negated_plane_ast_expr(name: &str) -> ast::Expr {
5494 ast::Expr::UnaryExpression(Box::new(ast::UnaryExpression::new(
5495 ast::UnaryOperator::Neg,
5496 ast::BinaryPart::Name(Box::new(ast_name(name.to_owned()))),
5497 )))
5498}
5499
5500fn create_face_of_ast(solid_expr: ast::Expr, face_expr: ast::Expr) -> ast::Expr {
5501 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5502 callee: ast::Node::no_src(ast_sketch2_name("faceOf")),
5503 unlabeled: Some(solid_expr),
5504 arguments: vec![ast::LabeledArg {
5505 label: Some(ast::Identifier::new("face")),
5506 arg: face_expr,
5507 }],
5508 digest: None,
5509 non_code_meta: Default::default(),
5510 })))
5511}
5512
5513fn region_name_from_sweep_variable(ast: &ast::Node<ast::Program>, sweep_variable_name: &str) -> Option<String> {
5514 let ast::Definition::Variable(sweep_decl) = ast.get_variable(sweep_variable_name)? else {
5515 return None;
5516 };
5517 let ast::Expr::CallExpressionKw(sweep_call) = &sweep_decl.init else {
5518 return None;
5519 };
5520 if !matches!(
5521 sweep_call.callee.name.name.as_str(),
5522 "extrude" | "revolve" | "sweep" | "loft"
5523 ) {
5524 return None;
5525 }
5526 let ast::Expr::Name(region_name_expr) = sweep_call.unlabeled.as_ref()? else {
5527 return None;
5528 };
5529 let candidate = region_name_expr.name.name.clone();
5530 let ast::Definition::Variable(region_decl) = ast.get_variable(&candidate)? else {
5531 return None;
5532 };
5533 let ast::Expr::CallExpressionKw(region_call) = ®ion_decl.init else {
5534 return None;
5535 };
5536 if region_call.callee.name.name != "region" {
5537 return None;
5538 }
5539 Some(candidate)
5540}
5541
5542fn get_or_insert_ast_reference(
5549 ast: &mut ast::Node<ast::Program>,
5550 source_ref: &SourceRef,
5551 prefix: &str,
5552 property: Option<&str>,
5553) -> Result<ast::Expr, KclError> {
5554 let command = AstMutateCommand::AddVariableDeclaration {
5555 prefix: prefix.to_owned(),
5556 };
5557 let ret = match mutate_ast_node_by_source_ref(ast, source_ref, command) {
5558 Ok((_, ret)) => ret,
5559 Err(err) => {
5560 if let Some(var_name) = variable_name_containing_source_ref(ast, source_ref) {
5561 AstMutateCommandReturn::Name(var_name)
5562 } else {
5563 return Err(err);
5564 }
5565 }
5566 };
5567 let AstMutateCommandReturn::Name(var_name) = ret else {
5568 return Err(KclError::refactor(
5569 "Expected variable name returned from AddVariableDeclaration".to_owned(),
5570 ));
5571 };
5572 let var_expr = ast::Expr::Name(Box::new(ast::Name::new(&var_name)));
5573 let Some(property) = property else {
5574 return Ok(var_expr);
5576 };
5577
5578 Ok(create_member_expression(var_expr, property))
5579}
5580
5581fn variable_name_containing_source_ref(ast: &ast::Node<ast::Program>, source_ref: &SourceRef) -> Option<String> {
5582 let source_range = match source_ref {
5583 SourceRef::Simple { range, .. } => *range,
5584 SourceRef::BackTrace { ranges } => {
5585 let [range] = ranges.as_slice() else {
5586 return None;
5587 };
5588 range.0
5589 }
5590 };
5591 ast.body.iter().find_map(|item| {
5592 let ast::BodyItem::VariableDeclaration(var_decl) = item else {
5593 return None;
5594 };
5595 let init_range = SourceRange::from(&var_decl.declaration.init);
5596 let source_is_inside_init = init_range.module_id() == source_range.module_id()
5597 && init_range.start() <= source_range.start()
5598 && source_range.end() <= init_range.end();
5599 if matches!(&var_decl.declaration.init, ast::Expr::SketchBlock(_))
5600 && init_range != source_range
5601 && source_is_inside_init
5602 {
5603 return None;
5604 }
5605 source_is_inside_init.then(|| var_decl.name().to_owned())
5606 })
5607}
5608
5609fn mutate_ast_node_by_source_ref(
5610 ast: &mut ast::Node<ast::Program>,
5611 source_ref: &SourceRef,
5612 command: AstMutateCommand,
5613) -> Result<(AstNodeRef, AstMutateCommandReturn), KclError> {
5614 let (source_range, node_path) = match source_ref {
5615 SourceRef::Simple { range, node_path } => (*range, node_path.clone()),
5616 SourceRef::BackTrace { ranges } => {
5617 let [range] = ranges.as_slice() else {
5618 return Err(KclError::refactor(format!(
5619 "Expected single source ref, got {}; ranges={ranges:#?}",
5620 ranges.len(),
5621 )));
5622 };
5623 (range.0, range.1.clone())
5624 }
5625 };
5626 let mut context = AstMutateContext {
5627 source_range,
5628 node_path,
5629 command,
5630 defined_names_stack: Default::default(),
5631 };
5632 let control = dfs_mut(ast, &mut context);
5633 match control {
5634 ControlFlow::Continue(_) => Err(KclError::refactor(
5635 "Could not find the KCL source for this edit. Try reloading the app, or update from code.".to_owned(),
5636 )),
5637 ControlFlow::Break(break_value) => break_value,
5638 }
5639}
5640
5641#[derive(Debug)]
5642struct AstMutateContext {
5643 source_range: SourceRange,
5644 node_path: Option<ast::NodePath>,
5645 command: AstMutateCommand,
5646 defined_names_stack: Vec<HashSet<String>>,
5647}
5648
5649#[derive(Debug)]
5650#[allow(clippy::large_enum_variant)]
5651enum AstMutateCommand {
5652 AddSketchBlockExprStmt {
5654 expr: ast::Expr,
5655 },
5656 AddSketchBlockVarDecl {
5658 prefix: String,
5659 expr: ast::Expr,
5660 },
5661 AddVariableDeclaration {
5662 prefix: String,
5663 },
5664 EditPoint {
5665 at: ast::Expr,
5666 },
5667 EditLine {
5668 start: ast::Expr,
5669 end: ast::Expr,
5670 construction: Option<bool>,
5671 },
5672 EditArc {
5673 start: ast::Expr,
5674 end: ast::Expr,
5675 center: ast::Expr,
5676 construction: Option<bool>,
5677 },
5678 EditCircle {
5679 start: ast::Expr,
5680 center: ast::Expr,
5681 construction: Option<bool>,
5682 },
5683 EditControlPointSpline {
5684 points: ast::Expr,
5685 construction: Option<bool>,
5686 },
5687 EditConstraintValue {
5688 value: ast::BinaryPart,
5689 },
5690 EditDistanceConstraintLabelPosition {
5691 label_position: ast::Expr,
5692 },
5693 EditCallUnlabeled {
5694 arg: ast::Expr,
5695 },
5696 EditVarInitialValue {
5697 value: Number,
5698 },
5699 DeleteNode,
5700}
5701
5702impl AstMutateCommand {
5703 fn needs_defined_names_stack(&self) -> bool {
5704 matches!(
5705 self,
5706 AstMutateCommand::AddSketchBlockVarDecl { .. } | AstMutateCommand::AddVariableDeclaration { .. }
5707 )
5708 }
5709}
5710
5711#[derive(Debug)]
5712enum AstMutateCommandReturn {
5713 None,
5714 Name(String),
5715}
5716
5717#[derive(Debug, Clone)]
5718struct AstNodeRef {
5719 range: SourceRange,
5720 node_path: Option<ast::NodePath>,
5721}
5722
5723impl<T> From<&ast::Node<T>> for AstNodeRef {
5724 fn from(value: &ast::Node<T>) -> Self {
5725 AstNodeRef {
5726 range: value.into(),
5727 node_path: value.node_path.clone(),
5728 }
5729 }
5730}
5731
5732impl From<&ast::BodyItem> for AstNodeRef {
5733 fn from(value: &ast::BodyItem) -> Self {
5734 match value {
5735 ast::BodyItem::ImportStatement(node) => AstNodeRef {
5736 range: node.into(),
5737 node_path: node.node_path.clone(),
5738 },
5739 ast::BodyItem::ExpressionStatement(node) => AstNodeRef {
5740 range: node.into(),
5741 node_path: node.node_path.clone(),
5742 },
5743 ast::BodyItem::VariableDeclaration(node) => AstNodeRef {
5744 range: node.into(),
5745 node_path: node.node_path.clone(),
5746 },
5747 ast::BodyItem::TypeDeclaration(node) => AstNodeRef {
5748 range: node.into(),
5749 node_path: node.node_path.clone(),
5750 },
5751 ast::BodyItem::ReturnStatement(node) => AstNodeRef {
5752 range: node.into(),
5753 node_path: node.node_path.clone(),
5754 },
5755 }
5756 }
5757}
5758
5759impl From<&ast::Expr> for AstNodeRef {
5760 fn from(value: &ast::Expr) -> Self {
5761 AstNodeRef {
5762 range: SourceRange::from(value),
5763 node_path: value.node_path().cloned(),
5764 }
5765 }
5766}
5767
5768impl From<&AstMutateContext> for AstNodeRef {
5769 fn from(value: &AstMutateContext) -> Self {
5770 AstNodeRef {
5771 range: value.source_range,
5772 node_path: value.node_path.clone(),
5773 }
5774 }
5775}
5776
5777impl TryFrom<&NodeMut<'_>> for AstNodeRef {
5778 type Error = crate::walk::AstNodeError;
5779
5780 fn try_from(value: &NodeMut<'_>) -> Result<Self, Self::Error> {
5781 Ok(AstNodeRef {
5782 range: SourceRange::try_from(value)?,
5783 node_path: value.try_into()?,
5784 })
5785 }
5786}
5787
5788impl From<AstNodeRef> for SourceRange {
5789 fn from(value: AstNodeRef) -> Self {
5790 value.range
5791 }
5792}
5793
5794impl Visitor for AstMutateContext {
5795 type Break = Result<(AstNodeRef, AstMutateCommandReturn), KclError>;
5796 type Continue = ();
5797
5798 fn visit(&mut self, node: NodeMut<'_>) -> TraversalReturn<Self::Break, Self::Continue> {
5799 filter_and_process(self, node)
5800 }
5801
5802 fn finish(&mut self, node: NodeMut<'_>) {
5803 match &node {
5804 NodeMut::Program(_) | NodeMut::SketchBlock(_) => {
5805 self.defined_names_stack.pop();
5806 }
5807 _ => {}
5808 }
5809 }
5810}
5811
5812fn filter_and_process(
5813 ctx: &mut AstMutateContext,
5814 node: NodeMut,
5815) -> TraversalReturn<Result<(AstNodeRef, AstMutateCommandReturn), KclError>> {
5816 let Ok(node_range) = SourceRange::try_from(&node) else {
5817 return TraversalReturn::new_continue(());
5819 };
5820 if let NodeMut::VariableDeclaration(var_decl) = &node {
5825 let expr_range = SourceRange::from(&var_decl.declaration.init);
5826 let expr_node_path = var_decl.declaration.init.node_path();
5827 if source_ref_matches(ctx, expr_range, expr_node_path) {
5828 if let AstMutateCommand::AddVariableDeclaration { .. } = &ctx.command {
5829 return TraversalReturn::new_break(Ok((
5832 AstNodeRef::from(&**var_decl),
5833 AstMutateCommandReturn::Name(var_decl.name().to_owned()),
5834 )));
5835 }
5836 if let AstMutateCommand::DeleteNode = &ctx.command {
5837 return TraversalReturn {
5840 mutate_body_item: MutateBodyItem::Delete,
5841 control_flow: ControlFlow::Break(Ok((AstNodeRef::from(&*ctx), AstMutateCommandReturn::None))),
5842 };
5843 }
5844 }
5845 }
5846 if let NodeMut::ExpressionStatement(expr_stmt) = &node {
5849 let expr_range = SourceRange::from(&expr_stmt.expression);
5850 let expr_node_path = expr_stmt.expression.node_path();
5851 if source_ref_matches(ctx, expr_range, expr_node_path) {
5852 if let AstMutateCommand::AddVariableDeclaration { .. } = &ctx.command {
5853 let Ok(node_ref) = AstNodeRef::try_from(&node) else {
5856 return TraversalReturn::new_continue(());
5857 };
5858 return process(ctx, node).map_break(|result| result.map(|cmd_return| (node_ref, cmd_return)));
5859 }
5860 if let AstMutateCommand::DeleteNode = &ctx.command {
5861 return TraversalReturn {
5864 mutate_body_item: MutateBodyItem::Delete,
5865 control_flow: ControlFlow::Break(Ok((AstNodeRef::from(&*ctx), AstMutateCommandReturn::None))),
5866 };
5867 }
5868 }
5869 }
5870
5871 if ctx.command.needs_defined_names_stack() {
5872 if let NodeMut::Program(program) = &node {
5873 ctx.defined_names_stack.push(find_defined_names(*program));
5874 } else if let NodeMut::SketchBlock(block) = &node {
5875 ctx.defined_names_stack.push(find_defined_names(&block.body));
5876 }
5877 }
5878
5879 let node_path = <Option<ast::NodePath>>::try_from(&node).ok().flatten();
5881 if !source_ref_matches(ctx, node_range, node_path.as_ref()) {
5882 return TraversalReturn::new_continue(());
5883 }
5884 let Ok(node_ref) = AstNodeRef::try_from(&node) else {
5885 return TraversalReturn::new_continue(());
5886 };
5887 process(ctx, node).map_break(|result| result.map(|cmd_return| (node_ref, cmd_return)))
5888}
5889
5890fn source_ref_matches(ctx: &AstMutateContext, node_range: SourceRange, node_path: Option<&ast::NodePath>) -> bool {
5891 match &ctx.node_path {
5892 Some(target) => Some(target) == node_path,
5893 None => node_range == ctx.source_range,
5894 }
5895}
5896
5897fn process(ctx: &AstMutateContext, node: NodeMut) -> TraversalReturn<Result<AstMutateCommandReturn, KclError>> {
5898 match &ctx.command {
5899 AstMutateCommand::AddSketchBlockExprStmt { expr } => {
5900 if let NodeMut::SketchBlock(sketch_block) = node {
5901 sketch_block
5902 .body
5903 .items
5904 .push(ast::BodyItem::ExpressionStatement(ast::Node {
5905 inner: ast::ExpressionStatement {
5906 expression: expr.clone(),
5907 digest: None,
5908 },
5909 start: Default::default(),
5910 end: Default::default(),
5911 module_id: Default::default(),
5912 node_path: None,
5913 outer_attrs: Default::default(),
5914 pre_comments: Default::default(),
5915 comment_start: Default::default(),
5916 }));
5917 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5918 }
5919 }
5920 AstMutateCommand::AddSketchBlockVarDecl { prefix, expr } => {
5921 if let NodeMut::SketchBlock(sketch_block) = node {
5922 let empty_defined_names = HashSet::new();
5923 let defined_names = ctx.defined_names_stack.last().unwrap_or(&empty_defined_names);
5924 let Ok(name) = next_free_name(prefix, defined_names) else {
5925 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5926 };
5927 sketch_block
5928 .body
5929 .items
5930 .push(ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(
5931 ast::VariableDeclaration::new(
5932 ast::VariableDeclarator::new(&name, expr.clone()),
5933 ast::ItemVisibility::Default,
5934 ast::VariableKind::Const,
5935 ),
5936 ))));
5937 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::Name(name)));
5938 }
5939 }
5940 AstMutateCommand::AddVariableDeclaration { prefix } => {
5941 if let NodeMut::VariableDeclaration(inner) = node {
5942 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::Name(inner.name().to_owned())));
5943 }
5944 if let NodeMut::ExpressionStatement(expr_stmt) = node {
5945 let empty_defined_names = HashSet::new();
5946 let defined_names = ctx.defined_names_stack.last().unwrap_or(&empty_defined_names);
5947 let Ok(name) = next_free_name(prefix, defined_names) else {
5948 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5950 };
5951 let mutate_node =
5952 ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(ast::VariableDeclaration::new(
5953 ast::VariableDeclarator::new(&name, expr_stmt.expression.clone()),
5954 ast::ItemVisibility::Default,
5955 ast::VariableKind::Const,
5956 ))));
5957 return TraversalReturn {
5958 mutate_body_item: MutateBodyItem::Mutate(Box::new(mutate_node)),
5959 control_flow: ControlFlow::Break(Ok(AstMutateCommandReturn::Name(name))),
5960 };
5961 }
5962 }
5963 AstMutateCommand::EditPoint { at } => {
5964 if let NodeMut::CallExpressionKw(call) = node {
5965 if call.callee.name.name != POINT_FN {
5966 return TraversalReturn::new_continue(());
5967 }
5968 for labeled_arg in &mut call.arguments {
5970 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(POINT_AT_PARAM) {
5971 labeled_arg.arg = at.clone();
5972 }
5973 }
5974 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5975 }
5976 }
5977 AstMutateCommand::EditLine {
5978 start,
5979 end,
5980 construction,
5981 } => {
5982 if let NodeMut::CallExpressionKw(call) = node {
5983 if call.callee.name.name != LINE_FN {
5984 return TraversalReturn::new_continue(());
5985 }
5986 for labeled_arg in &mut call.arguments {
5988 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(LINE_START_PARAM) {
5989 labeled_arg.arg = start.clone();
5990 }
5991 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(LINE_END_PARAM) {
5992 labeled_arg.arg = end.clone();
5993 }
5994 }
5995 if let Some(construction_value) = construction {
5997 let construction_exists = call
5998 .arguments
5999 .iter()
6000 .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
6001 if *construction_value {
6002 if construction_exists {
6004 for labeled_arg in &mut call.arguments {
6006 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
6007 labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
6008 value: ast::LiteralValue::Bool(true),
6009 raw: "true".to_string(),
6010 digest: None,
6011 })));
6012 }
6013 }
6014 } else {
6015 call.arguments.push(ast::LabeledArg {
6017 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
6018 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
6019 value: ast::LiteralValue::Bool(true),
6020 raw: "true".to_string(),
6021 digest: None,
6022 }))),
6023 });
6024 }
6025 } else {
6026 call.arguments
6028 .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
6029 }
6030 }
6031 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6032 }
6033 }
6034 AstMutateCommand::EditArc {
6035 start,
6036 end,
6037 center,
6038 construction,
6039 } => {
6040 if let NodeMut::CallExpressionKw(call) = node {
6041 if call.callee.name.name != ARC_FN {
6042 return TraversalReturn::new_continue(());
6043 }
6044 for labeled_arg in &mut call.arguments {
6046 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_START_PARAM) {
6047 labeled_arg.arg = start.clone();
6048 }
6049 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_END_PARAM) {
6050 labeled_arg.arg = end.clone();
6051 }
6052 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_CENTER_PARAM) {
6053 labeled_arg.arg = center.clone();
6054 }
6055 }
6056 if let Some(construction_value) = construction {
6058 let construction_exists = call
6059 .arguments
6060 .iter()
6061 .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
6062 if *construction_value {
6063 if construction_exists {
6065 for labeled_arg in &mut call.arguments {
6067 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
6068 labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
6069 value: ast::LiteralValue::Bool(true),
6070 raw: "true".to_string(),
6071 digest: None,
6072 })));
6073 }
6074 }
6075 } else {
6076 call.arguments.push(ast::LabeledArg {
6078 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
6079 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
6080 value: ast::LiteralValue::Bool(true),
6081 raw: "true".to_string(),
6082 digest: None,
6083 }))),
6084 });
6085 }
6086 } else {
6087 call.arguments
6089 .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
6090 }
6091 }
6092 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6093 }
6094 }
6095 AstMutateCommand::EditCircle {
6096 start,
6097 center,
6098 construction,
6099 } => {
6100 if let NodeMut::CallExpressionKw(call) = node {
6101 if call.callee.name.name != CIRCLE_FN {
6102 return TraversalReturn::new_continue(());
6103 }
6104 for labeled_arg in &mut call.arguments {
6106 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CIRCLE_START_PARAM) {
6107 labeled_arg.arg = start.clone();
6108 }
6109 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CIRCLE_CENTER_PARAM) {
6110 labeled_arg.arg = center.clone();
6111 }
6112 }
6113 if let Some(construction_value) = construction {
6115 let construction_exists = call
6116 .arguments
6117 .iter()
6118 .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
6119 if *construction_value {
6120 if construction_exists {
6121 for labeled_arg in &mut call.arguments {
6123 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
6124 labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
6125 value: ast::LiteralValue::Bool(true),
6126 raw: "true".to_string(),
6127 digest: None,
6128 })));
6129 }
6130 }
6131 } else {
6132 call.arguments.push(ast::LabeledArg {
6134 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
6135 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
6136 value: ast::LiteralValue::Bool(true),
6137 raw: "true".to_string(),
6138 digest: None,
6139 }))),
6140 });
6141 }
6142 } else {
6143 call.arguments
6145 .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
6146 }
6147 }
6148 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6149 }
6150 }
6151 AstMutateCommand::EditControlPointSpline { points, construction } => {
6152 if let NodeMut::CallExpressionKw(call) = node {
6153 if call.callee.name.name != CONTROL_POINT_SPLINE_FN {
6154 return TraversalReturn::new_continue(());
6155 }
6156 for labeled_arg in &mut call.arguments {
6157 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONTROL_POINT_SPLINE_POINTS_PARAM)
6158 {
6159 labeled_arg.arg = points.clone();
6160 }
6161 }
6162 if let Some(construction_value) = construction {
6164 let construction_exists = call
6165 .arguments
6166 .iter()
6167 .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
6168 if *construction_value {
6169 if construction_exists {
6170 for labeled_arg in &mut call.arguments {
6171 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
6172 labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
6173 value: ast::LiteralValue::Bool(true),
6174 raw: "true".to_string(),
6175 digest: None,
6176 })));
6177 }
6178 }
6179 } else {
6180 call.arguments.push(ast::LabeledArg {
6181 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
6182 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
6183 value: ast::LiteralValue::Bool(true),
6184 raw: "true".to_string(),
6185 digest: None,
6186 }))),
6187 });
6188 }
6189 } else {
6190 call.arguments
6191 .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
6192 }
6193 }
6194 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6195 }
6196 }
6197 AstMutateCommand::EditConstraintValue { value } => {
6198 if let NodeMut::BinaryExpression(binary_expr) = node {
6199 let left_is_constraint = matches!(
6200 &binary_expr.left,
6201 ast::BinaryPart::CallExpressionKw(call)
6202 if matches!(
6203 call.callee.name.name.as_str(),
6204 DISTANCE_FN | HORIZONTAL_DISTANCE_FN | VERTICAL_DISTANCE_FN | RADIUS_FN | DIAMETER_FN | ANGLE_FN
6205 )
6206 );
6207 if left_is_constraint {
6208 binary_expr.right = value.clone();
6209 } else {
6210 binary_expr.left = value.clone();
6211 }
6212
6213 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6214 }
6215 }
6216 AstMutateCommand::EditDistanceConstraintLabelPosition { label_position } => {
6217 if let NodeMut::BinaryExpression(binary_expr) = node {
6218 let ast::BinaryPart::CallExpressionKw(call) = &mut binary_expr.left else {
6219 return TraversalReturn::new_continue(());
6220 };
6221 if !matches!(
6222 call.callee.name.name.as_str(),
6223 DISTANCE_FN | HORIZONTAL_DISTANCE_FN | VERTICAL_DISTANCE_FN | RADIUS_FN | DIAMETER_FN
6224 ) {
6225 return TraversalReturn::new_continue(());
6226 }
6227
6228 if let Some(label_arg) = call
6229 .arguments
6230 .iter_mut()
6231 .find(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(LABEL_POSITION_PARAM))
6232 {
6233 label_arg.arg = label_position.clone();
6234 } else {
6235 call.arguments.push(ast::LabeledArg {
6236 label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
6237 arg: label_position.clone(),
6238 });
6239 }
6240
6241 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6242 }
6243 }
6244 AstMutateCommand::EditCallUnlabeled { arg } => {
6245 if let NodeMut::CallExpressionKw(call) = node {
6246 call.unlabeled = Some(arg.clone());
6247 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6248 }
6249 }
6250 AstMutateCommand::EditVarInitialValue { value } => {
6251 if let NodeMut::SketchVar(sketch_var) = node {
6255 let Ok(literal) = to_source_number(*value) else {
6256 return TraversalReturn::new_break(Err(KclError::refactor(format!(
6257 "Could not convert number to AST literal: {:?}",
6258 *value
6259 ))));
6260 };
6261 sketch_var.initial = Some(Box::new(ast::Node::no_src(literal)));
6262 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6263 }
6264 }
6265 AstMutateCommand::DeleteNode => {
6266 return TraversalReturn {
6267 mutate_body_item: MutateBodyItem::Delete,
6268 control_flow: ControlFlow::Break(Ok(AstMutateCommandReturn::None)),
6269 };
6270 }
6271 }
6272 TraversalReturn::new_continue(())
6273}
6274
6275struct FindSketchBlockSourceRange {
6276 target_before_mutation: SourceRange,
6278 found: Cell<Option<AstNodeRef>>,
6282}
6283
6284impl<'a> crate::walk::Visitor<'a> for &FindSketchBlockSourceRange {
6285 type Error = crate::front::Error;
6286
6287 fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
6288 let Ok(node_range) = SourceRange::try_from(&node) else {
6289 return Ok(true);
6290 };
6291
6292 if let crate::walk::Node::SketchBlock(sketch_block) = node {
6293 if node_range.module_id() == self.target_before_mutation.module_id()
6294 && node_range.start() == self.target_before_mutation.start()
6295 && node_range.end() >= self.target_before_mutation.end()
6297 {
6298 self.found.set(sketch_block.body.items.last().map(|item| match item {
6299 ast::BodyItem::VariableDeclaration(node) => AstNodeRef::from(&node.declaration.init),
6303 _ => AstNodeRef::from(item),
6304 }));
6305 return Ok(false);
6306 } else {
6307 return Ok(true);
6310 }
6311 }
6312
6313 for child in node.children().iter() {
6314 if !child.visit(*self)? {
6315 return Ok(false);
6316 }
6317 }
6318
6319 Ok(true)
6320 }
6321}
6322
6323struct FindSketchBlockByNodePath {
6324 target_node_path: ast::NodePath,
6326 found: Cell<Option<AstNodeRef>>,
6330}
6331
6332impl<'a> crate::walk::Visitor<'a> for &FindSketchBlockByNodePath {
6333 type Error = crate::front::Error;
6334
6335 fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
6336 let Ok(node_path) = <Option<ast::NodePath>>::try_from(&node) else {
6337 return Ok(true);
6338 };
6339
6340 if let crate::walk::Node::SketchBlock(sketch_block) = node {
6341 if let Some(node_path) = node_path
6342 && node_path == self.target_node_path
6343 {
6344 self.found.set(sketch_block.body.items.last().map(|item| match item {
6345 ast::BodyItem::VariableDeclaration(node) => AstNodeRef::from(&node.declaration.init),
6349 _ => AstNodeRef::from(item),
6350 }));
6351
6352 return Ok(false);
6353 } else {
6354 return Ok(true);
6357 }
6358 }
6359
6360 for child in node.children().iter() {
6361 if !child.visit(*self)? {
6362 return Ok(false);
6363 }
6364 }
6365
6366 Ok(true)
6367 }
6368}
6369
6370fn find_sketch_block_added_item(
6378 ast: &ast::Node<ast::Program>,
6379 sketch_block_before_mutation: &AstNodeRef,
6380) -> Result<AstNodeRef, KclError> {
6381 if let Some(node_path) = &sketch_block_before_mutation.node_path {
6382 let find = FindSketchBlockByNodePath {
6383 target_node_path: node_path.clone(),
6384 found: Cell::new(None),
6385 };
6386 let node = crate::walk::Node::from(ast);
6387 node.visit(&find).map_err(|err| KclError::refactor(err.msg))?;
6388 find.found.into_inner().ok_or_else(|| {
6389 KclError::refactor(format!(
6390 "Node ID after mutation not found for Node ID before mutation: {node_path:?}"
6391 ))
6392 })
6393 } else {
6394 let find = FindSketchBlockSourceRange {
6396 target_before_mutation: sketch_block_before_mutation.range,
6397 found: Cell::new(None),
6398 };
6399 let node = crate::walk::Node::from(ast);
6400 node.visit(&find).map_err(|err| KclError::refactor(err.msg))?;
6401 find.found.into_inner().ok_or_else(|| KclError::refactor(
6402 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?"),
6403 ))
6404 }
6405}
6406
6407fn source_from_ast(ast: &ast::Node<ast::Program>) -> String {
6408 ast.recast_top(&Default::default(), 0)
6410}
6411
6412struct FindNumericLiteral {
6413 target: SourceRange,
6414 found: Cell<Option<ast::NumericLiteral>>,
6415}
6416
6417impl<'a> crate::walk::Visitor<'a> for &FindNumericLiteral {
6418 type Error = crate::front::Error;
6419
6420 fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
6421 let Ok(node_range) = SourceRange::try_from(&node) else {
6422 return Ok(true);
6423 };
6424
6425 if node_range == self.target
6426 && let crate::walk::Node::NumericLiteral(literal) = node
6427 {
6428 self.found.set(Some(literal.inner.clone()));
6429 return Ok(false);
6430 }
6431
6432 for child in node.children().iter() {
6433 if !child.visit(*self)? {
6434 return Ok(false);
6435 }
6436 }
6437
6438 Ok(true)
6439 }
6440}
6441
6442fn numeric_literal_at_source_range(ast: &ast::Node<ast::Program>, target: SourceRange) -> Option<ast::NumericLiteral> {
6443 let find = FindNumericLiteral {
6444 target,
6445 found: Cell::new(None),
6446 };
6447 let node = crate::walk::Node::from(ast);
6448 node.visit(&find).ok()?;
6449 find.found.into_inner()
6450}
6451
6452struct FindSketchVarInitialByNodePath<'a> {
6453 target: &'a ast::NodePath,
6454 sketch_var_found: Cell<bool>,
6455 initial_literal: Cell<Option<ast::NumericLiteral>>,
6456}
6457
6458impl<'a, 'b> crate::walk::Visitor<'b> for &FindSketchVarInitialByNodePath<'a> {
6459 type Error = crate::front::Error;
6460
6461 fn visit_node(&self, node: crate::walk::Node<'b>) -> anyhow::Result<bool, Self::Error> {
6462 if let crate::walk::Node::SketchVar(sketch_var) = node
6463 && sketch_var.node_path.as_ref() == Some(self.target)
6464 {
6465 self.sketch_var_found.set(true);
6466 if let Some(initial) = &sketch_var.initial {
6467 self.initial_literal.set(Some(initial.inner.clone()));
6468 }
6469 return Ok(false);
6470 }
6471
6472 for child in node.children().iter() {
6473 if !child.visit(*self)? {
6474 return Ok(false);
6475 }
6476 }
6477
6478 Ok(true)
6479 }
6480}
6481
6482fn numeric_literal_at_node_path(
6492 ast: &ast::Node<ast::Program>,
6493 node_path: Option<&ast::NodePath>,
6494 source_range: SourceRange,
6495) -> Option<Option<ast::NumericLiteral>> {
6496 let Some(node_path) = node_path else {
6497 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";
6498 #[cfg(target_arch = "wasm32")]
6499 web_sys::console::warn_1(&message.into());
6500 #[cfg(not(target_arch = "wasm32"))]
6501 eprintln!("WARNING: {message}");
6502 return numeric_literal_at_source_range(ast, source_range).map(Some);
6503 };
6504 let find = FindSketchVarInitialByNodePath {
6505 target: node_path,
6506 sketch_var_found: Cell::new(false),
6507 initial_literal: Cell::new(None),
6508 };
6509 let node = crate::walk::Node::from(ast);
6510 node.visit(&find).ok()?;
6511 if !find.sketch_var_found.get() {
6512 return None;
6513 }
6514 Some(find.initial_literal.into_inner())
6515}
6516
6517fn suffix_length_unit(suffix: NumericSuffix) -> Option<UnitLength> {
6518 match suffix {
6519 NumericSuffix::Mm => Some(UnitLength::Millimeters),
6520 NumericSuffix::Cm => Some(UnitLength::Centimeters),
6521 NumericSuffix::M => Some(UnitLength::Meters),
6522 NumericSuffix::Inch => Some(UnitLength::Inches),
6523 NumericSuffix::Ft => Some(UnitLength::Feet),
6524 NumericSuffix::Yd => Some(UnitLength::Yards),
6525 _ => None,
6526 }
6527}
6528
6529fn number_value_in_default_length_units(number: Number, default_length_unit: UnitLength) -> f64 {
6530 match suffix_length_unit(number.units) {
6531 Some(unit) => adjust_length(unit, number.value, default_length_unit).0,
6532 None => number.value,
6533 }
6534}
6535
6536fn literal_value_in_default_length_units(literal: &ast::NumericLiteral, default_length_unit: UnitLength) -> f64 {
6537 match suffix_length_unit(literal.suffix) {
6538 Some(unit) => adjust_length(unit, literal.value, default_length_unit).0,
6539 None => literal.value,
6540 }
6541}
6542
6543fn var_solution_needs_commit(
6544 current_literal: &ast::NumericLiteral,
6545 solved_value: Number,
6546 default_length_unit: UnitLength,
6547) -> bool {
6548 let current = literal_value_in_default_length_units(current_literal, default_length_unit);
6549 let solved = number_value_in_default_length_units(solved_value, default_length_unit);
6550
6551 (current - solved).abs() > 1e-9
6552}
6553
6554fn preserve_var_solution_literal_style(
6555 current_literal: &ast::NumericLiteral,
6556 solved_value: Number,
6557 default_length_unit: UnitLength,
6558) -> Number {
6559 if current_literal.suffix == NumericSuffix::None {
6560 return Number {
6561 value: number_value_in_default_length_units(solved_value, default_length_unit),
6562 units: NumericSuffix::None,
6563 };
6564 }
6565
6566 let Some(current_unit) = suffix_length_unit(current_literal.suffix) else {
6567 return solved_value;
6568 };
6569
6570 let solved_default_value = number_value_in_default_length_units(solved_value, default_length_unit);
6571 Number {
6572 value: adjust_length(default_length_unit, solved_default_value, current_unit).0,
6573 units: current_literal.suffix,
6574 }
6575}
6576
6577pub(crate) fn to_ast_point2d(point: &Point2d<Expr>) -> anyhow::Result<ast::Expr> {
6578 Ok(ast::Expr::ArrayExpression(Box::new(ast::Node {
6579 inner: ast::ArrayExpression {
6580 elements: vec![to_source_expr(&point.x)?, to_source_expr(&point.y)?],
6581 non_code_meta: Default::default(),
6582 digest: None,
6583 },
6584 start: Default::default(),
6585 end: Default::default(),
6586 module_id: Default::default(),
6587 node_path: None,
6588 outer_attrs: Default::default(),
6589 pre_comments: Default::default(),
6590 comment_start: Default::default(),
6591 })))
6592}
6593
6594pub(crate) fn to_ast_point2d_array(points: &[Point2d<Expr>]) -> anyhow::Result<ast::Expr> {
6595 Ok(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
6596 ast::ArrayExpression {
6597 elements: points.iter().map(to_ast_point2d).collect::<anyhow::Result<Vec<_>>>()?,
6598 digest: None,
6599 non_code_meta: Default::default(),
6600 },
6601 ))))
6602}
6603
6604fn to_ast_point2d_number(point: &Point2d<Number>) -> anyhow::Result<ast::Expr> {
6605 Ok(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
6606 ast::ArrayExpression {
6607 elements: vec![
6608 ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(to_source_number(
6609 point.x,
6610 )?)))),
6611 ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(to_source_number(
6612 point.y,
6613 )?)))),
6614 ],
6615 non_code_meta: Default::default(),
6616 digest: None,
6617 },
6618 ))))
6619}
6620
6621fn to_source_expr(expr: &Expr) -> anyhow::Result<ast::Expr> {
6622 match expr {
6623 Expr::Number(number) => Ok(ast::Expr::Literal(Box::new(ast::Node {
6624 inner: ast::Literal::from(to_source_number(*number)?),
6625 start: Default::default(),
6626 end: Default::default(),
6627 module_id: Default::default(),
6628 node_path: None,
6629 outer_attrs: Default::default(),
6630 pre_comments: Default::default(),
6631 comment_start: Default::default(),
6632 }))),
6633 Expr::Var(number) => Ok(ast::Expr::SketchVar(Box::new(ast::Node {
6634 inner: ast::SketchVar {
6635 initial: Some(Box::new(ast::Node {
6636 inner: to_source_number(*number)?,
6637 start: Default::default(),
6638 end: Default::default(),
6639 module_id: Default::default(),
6640 node_path: None,
6641 outer_attrs: Default::default(),
6642 pre_comments: Default::default(),
6643 comment_start: Default::default(),
6644 })),
6645 digest: None,
6646 },
6647 start: Default::default(),
6648 end: Default::default(),
6649 module_id: Default::default(),
6650 node_path: None,
6651 outer_attrs: Default::default(),
6652 pre_comments: Default::default(),
6653 comment_start: Default::default(),
6654 }))),
6655 Expr::Variable(variable) => Ok(ast_name_expr(variable.clone())),
6656 }
6657}
6658
6659fn to_source_number(number: Number) -> anyhow::Result<ast::NumericLiteral> {
6660 Ok(ast::NumericLiteral {
6661 value: number.value,
6662 suffix: number.units,
6663 raw: format_number_literal(number.value, number.units, None)?,
6664 digest: None,
6665 })
6666}
6667
6668pub(crate) fn ast_name_expr(name: String) -> ast::Expr {
6669 ast::Expr::Name(Box::new(ast_name(name)))
6670}
6671
6672fn ast_name(name: String) -> ast::Node<ast::Name> {
6673 ast::Node {
6674 inner: ast::Name {
6675 name: ast::Node {
6676 inner: ast::Identifier { name, digest: None },
6677 start: Default::default(),
6678 end: Default::default(),
6679 module_id: Default::default(),
6680 node_path: None,
6681 outer_attrs: Default::default(),
6682 pre_comments: Default::default(),
6683 comment_start: Default::default(),
6684 },
6685 path: Vec::new(),
6686 abs_path: false,
6687 digest: None,
6688 },
6689 start: Default::default(),
6690 end: Default::default(),
6691 module_id: Default::default(),
6692 node_path: None,
6693 outer_attrs: Default::default(),
6694 pre_comments: Default::default(),
6695 comment_start: Default::default(),
6696 }
6697}
6698
6699pub(crate) fn ast_sketch2_name(name: &str) -> ast::Name {
6700 ast::Name {
6701 name: ast::Node {
6702 inner: ast::Identifier {
6703 name: name.to_owned(),
6704 digest: None,
6705 },
6706 start: Default::default(),
6707 end: Default::default(),
6708 module_id: Default::default(),
6709 node_path: None,
6710 outer_attrs: Default::default(),
6711 pre_comments: Default::default(),
6712 comment_start: Default::default(),
6713 },
6714 path: Default::default(),
6715 abs_path: false,
6716 digest: None,
6717 }
6718}
6719
6720pub(crate) fn create_coincident_ast(exprs: impl IntoIterator<Item = ast::Expr>) -> ast::Expr {
6724 let elements = exprs.into_iter().collect::<Vec<_>>();
6725 debug_assert!(elements.len() >= 2, "Coincident AST should have at least 2 inputs");
6726
6727 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
6729 elements,
6730 digest: None,
6731 non_code_meta: Default::default(),
6732 })));
6733
6734 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6736 callee: ast::Node::no_src(ast_sketch2_name(COINCIDENT_FN)),
6737 unlabeled: Some(array_expr),
6738 arguments: Default::default(),
6739 digest: None,
6740 non_code_meta: Default::default(),
6741 })))
6742}
6743
6744pub(crate) fn create_line_ast(start_ast: ast::Expr, end_ast: ast::Expr) -> ast::Expr {
6746 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6747 callee: ast::Node::no_src(ast_sketch2_name(LINE_FN)),
6748 unlabeled: None,
6749 arguments: vec![
6750 ast::LabeledArg {
6751 label: Some(ast::Identifier::new(LINE_START_PARAM)),
6752 arg: start_ast,
6753 },
6754 ast::LabeledArg {
6755 label: Some(ast::Identifier::new(LINE_END_PARAM)),
6756 arg: end_ast,
6757 },
6758 ],
6759 digest: None,
6760 non_code_meta: Default::default(),
6761 })))
6762}
6763
6764pub(crate) fn create_arc_ast(start_ast: ast::Expr, end_ast: ast::Expr, center_ast: ast::Expr) -> ast::Expr {
6766 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6767 callee: ast::Node::no_src(ast_sketch2_name(ARC_FN)),
6768 unlabeled: None,
6769 arguments: vec![
6770 ast::LabeledArg {
6771 label: Some(ast::Identifier::new(ARC_START_PARAM)),
6772 arg: start_ast,
6773 },
6774 ast::LabeledArg {
6775 label: Some(ast::Identifier::new(ARC_END_PARAM)),
6776 arg: end_ast,
6777 },
6778 ast::LabeledArg {
6779 label: Some(ast::Identifier::new(ARC_CENTER_PARAM)),
6780 arg: center_ast,
6781 },
6782 ],
6783 digest: None,
6784 non_code_meta: Default::default(),
6785 })))
6786}
6787
6788pub(crate) fn create_circle_ast(start_ast: ast::Expr, center_ast: ast::Expr) -> ast::Expr {
6790 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6791 callee: ast::Node::no_src(ast_sketch2_name(CIRCLE_FN)),
6792 unlabeled: None,
6793 arguments: vec![
6794 ast::LabeledArg {
6795 label: Some(ast::Identifier::new(CIRCLE_START_PARAM)),
6796 arg: start_ast,
6797 },
6798 ast::LabeledArg {
6799 label: Some(ast::Identifier::new(CIRCLE_CENTER_PARAM)),
6800 arg: center_ast,
6801 },
6802 ],
6803 digest: None,
6804 non_code_meta: Default::default(),
6805 })))
6806}
6807
6808pub(crate) fn create_horizontal_ast(line_expr: ast::Expr) -> ast::Expr {
6810 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6811 callee: ast::Node::no_src(ast_sketch2_name(HORIZONTAL_FN)),
6812 unlabeled: Some(line_expr),
6813 arguments: Default::default(),
6814 digest: None,
6815 non_code_meta: Default::default(),
6816 })))
6817}
6818
6819pub(crate) fn create_vertical_ast(line_expr: 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(VERTICAL_FN)),
6823 unlabeled: Some(line_expr),
6824 arguments: Default::default(),
6825 digest: None,
6826 non_code_meta: Default::default(),
6827 })))
6828}
6829
6830pub(crate) fn create_member_expression(object_expr: ast::Expr, property: &str) -> ast::Expr {
6832 ast::Expr::MemberExpression(Box::new(ast::Node::no_src(ast::MemberExpression {
6833 object: object_expr,
6834 property: ast::Expr::Name(Box::new(ast::Node::no_src(ast::Name {
6835 name: ast::Node::no_src(ast::Identifier {
6836 name: property.to_string(),
6837 digest: None,
6838 }),
6839 path: Vec::new(),
6840 abs_path: false,
6841 digest: None,
6842 }))),
6843 computed: false,
6844 digest: None,
6845 })))
6846}
6847
6848pub(crate) fn create_index_expression(object_expr: ast::Expr, index: usize) -> ast::Expr {
6849 ast::Expr::MemberExpression(Box::new(ast::Node::no_src(ast::MemberExpression {
6850 object: object_expr,
6851 property: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(ast::NumericLiteral {
6852 value: index as f64,
6853 suffix: NumericSuffix::None,
6854 raw: index.to_string(),
6855 digest: None,
6856 })))),
6857 computed: true,
6858 digest: None,
6859 })))
6860}
6861
6862fn create_fixed_point_constraint_ast(point_expr: ast::Expr, position: Point2d<Number>) -> anyhow::Result<ast::Expr> {
6864 let x_literal = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(to_source_number(
6866 position.x,
6867 )?))));
6868 let y_literal = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(to_source_number(
6869 position.y,
6870 )?))));
6871 let point_array = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
6872 elements: vec![x_literal, y_literal],
6873 digest: None,
6874 non_code_meta: Default::default(),
6875 })));
6876
6877 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
6879 elements: vec![point_expr, point_array],
6880 digest: None,
6881 non_code_meta: Default::default(),
6882 })));
6883
6884 Ok(ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(
6886 ast::CallExpressionKw {
6887 callee: ast::Node::no_src(ast_sketch2_name(FIXED_FN)),
6888 unlabeled: Some(array_expr),
6889 arguments: Default::default(),
6890 digest: None,
6891 non_code_meta: Default::default(),
6892 },
6893 ))))
6894}
6895
6896pub(crate) fn create_equal_length_ast(line_exprs: Vec<ast::Expr>) -> ast::Expr {
6898 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
6899 elements: line_exprs,
6900 digest: None,
6901 non_code_meta: Default::default(),
6902 })));
6903
6904 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6906 callee: ast::Node::no_src(ast_sketch2_name(EQUAL_LENGTH_FN)),
6907 unlabeled: Some(array_expr),
6908 arguments: Default::default(),
6909 digest: None,
6910 non_code_meta: Default::default(),
6911 })))
6912}
6913
6914pub(crate) fn create_equal_radius_ast(segment_exprs: Vec<ast::Expr>) -> ast::Expr {
6916 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
6917 elements: segment_exprs,
6918 digest: None,
6919 non_code_meta: Default::default(),
6920 })));
6921
6922 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6923 callee: ast::Node::no_src(ast_sketch2_name(EQUAL_RADIUS_FN)),
6924 unlabeled: Some(array_expr),
6925 arguments: Default::default(),
6926 digest: None,
6927 non_code_meta: Default::default(),
6928 })))
6929}
6930
6931pub(crate) fn create_tangent_ast(seg1_expr: ast::Expr, seg2_expr: ast::Expr) -> ast::Expr {
6933 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
6934 elements: vec![seg1_expr, seg2_expr],
6935 digest: None,
6936 non_code_meta: Default::default(),
6937 })));
6938
6939 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6940 callee: ast::Node::no_src(ast_sketch2_name(TANGENT_FN)),
6941 unlabeled: Some(array_expr),
6942 arguments: Default::default(),
6943 digest: None,
6944 non_code_meta: Default::default(),
6945 })))
6946}
6947
6948pub(crate) fn create_symmetric_ast(input_exprs: Vec<ast::Expr>, axis_expr: ast::Expr) -> ast::Expr {
6950 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
6951 elements: input_exprs,
6952 digest: None,
6953 non_code_meta: Default::default(),
6954 })));
6955 let arguments = vec![ast::LabeledArg {
6956 label: Some(ast::Identifier::new(SYMMETRIC_AXIS_PARAM)),
6957 arg: axis_expr,
6958 }];
6959
6960 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6961 callee: ast::Node::no_src(ast_sketch2_name(SYMMETRIC_FN)),
6962 unlabeled: Some(array_expr),
6963 arguments,
6964 digest: None,
6965 non_code_meta: Default::default(),
6966 })))
6967}
6968
6969pub(crate) fn create_midpoint_ast(segment_expr: ast::Expr, point_expr: ast::Expr) -> ast::Expr {
6971 let arguments = vec![ast::LabeledArg {
6972 label: Some(ast::Identifier::new(MIDPOINT_POINT_PARAM)),
6973 arg: point_expr,
6974 }];
6975
6976 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6977 callee: ast::Node::no_src(ast_sketch2_name(MIDPOINT_FN)),
6978 unlabeled: Some(segment_expr),
6979 arguments,
6980 digest: None,
6981 non_code_meta: Default::default(),
6982 })))
6983}
6984
6985#[cfg(test)]
6986mod tests {
6987 use std::sync;
6988
6989 use super::*;
6990 use crate::engine::PlaneName;
6991 use crate::engine::engine_manager::EngineManager;
6992 use crate::execution::cache::SketchModeState;
6993 use crate::execution::cache::clear_mem_cache;
6994 use crate::execution::cache::read_old_memory;
6995 use crate::execution::cache::write_old_memory;
6996 use crate::front::Distance;
6997 use crate::front::Fixed;
6998 use crate::front::FixedPoint;
6999 use crate::front::Midpoint;
7000 use crate::front::Object;
7001 use crate::front::Plane;
7002 use crate::front::Sketch;
7003 use crate::front::Tangent;
7004 use crate::frontend::sketch::Vertical;
7005 use crate::pretty::NumericSuffix;
7006
7007 fn find_first_sketch_object(scene_graph: &SceneGraph) -> Option<&Object> {
7008 for object in &scene_graph.objects {
7009 if let ObjectKind::Sketch(_) = &object.kind {
7010 return Some(object);
7011 }
7012 }
7013 None
7014 }
7015
7016 fn find_first_face_object(scene_graph: &SceneGraph) -> Option<&Object> {
7017 for object in &scene_graph.objects {
7018 if let ObjectKind::Face(_) = &object.kind {
7019 return Some(object);
7020 }
7021 }
7022 None
7023 }
7024
7025 fn find_first_wall_object_id(scene_graph: &SceneGraph) -> Option<ObjectId> {
7026 for object in &scene_graph.objects {
7027 if matches!(&object.kind, ObjectKind::Wall(_)) {
7028 return Some(object.id);
7029 }
7030 }
7031 None
7032 }
7033
7034 #[test]
7035 fn test_region_name_from_sweep_variable_supports_sweep_kinds() {
7036 let source = "\
7037region001 = region(point = [0.1, 0.1], sketch = s)
7038extrude001 = extrude(region001, length = 5)
7039revolve001 = revolve(region001, axis = Y)
7040sweep001 = sweep(region001, path = path001)
7041loft001 = loft(region001)
7042not_sweep001 = shell(extrude001, faces = [], thickness = 1)
7043";
7044
7045 let program = Program::parse(source).unwrap().0.unwrap();
7046
7047 assert_eq!(
7048 region_name_from_sweep_variable(&program.ast, "extrude001"),
7049 Some("region001".to_owned())
7050 );
7051 assert_eq!(
7052 region_name_from_sweep_variable(&program.ast, "revolve001"),
7053 Some("region001".to_owned())
7054 );
7055 assert_eq!(
7056 region_name_from_sweep_variable(&program.ast, "sweep001"),
7057 Some("region001".to_owned())
7058 );
7059 assert_eq!(
7060 region_name_from_sweep_variable(&program.ast, "loft001"),
7061 Some("region001".to_owned())
7062 );
7063 assert_eq!(region_name_from_sweep_variable(&program.ast, "not_sweep001"), None);
7064 }
7065
7066 #[track_caller]
7067 fn expect_sketch(object: &Object) -> &Sketch {
7068 if let ObjectKind::Sketch(sketch) = &object.kind {
7069 sketch
7070 } else {
7071 panic!("Object is not a sketch: {:?}", object);
7072 }
7073 }
7074
7075 fn point_position(scene_graph: &SceneGraph, point_id: ObjectId) -> Point2d<Number> {
7076 let point_object = scene_graph.objects.get(point_id.0).unwrap();
7077 let ObjectKind::Segment {
7078 segment: Segment::Point(point),
7079 } = &point_object.kind
7080 else {
7081 panic!("Object is not a point segment: {point_object:?}");
7082 };
7083 point.position.clone()
7084 }
7085
7086 fn assert_point_position_close(actual: Point2d<Number>, expected: Point2d<Number>) {
7087 assert!((actual.x.value - expected.x.value).abs() < 1e-6);
7088 assert!((actual.y.value - expected.y.value).abs() < 1e-6);
7089 }
7090
7091 fn point_expr_mm(x: f64, y: f64) -> Point2d<Expr> {
7094 Point2d {
7095 x: Expr::Var(Number {
7096 value: x,
7097 units: NumericSuffix::Mm,
7098 }),
7099 y: Expr::Var(Number {
7100 value: y,
7101 units: NumericSuffix::Mm,
7102 }),
7103 }
7104 }
7105
7106 fn point_number_mm(x: f64, y: f64) -> Point2d<Number> {
7109 Point2d {
7110 x: Number {
7111 value: x,
7112 units: NumericSuffix::Mm,
7113 },
7114 y: Number {
7115 value: y,
7116 units: NumericSuffix::Mm,
7117 },
7118 }
7119 }
7120
7121 fn make_line_ctor(start_x: f64, start_y: f64, end_x: f64, end_y: f64, units: NumericSuffix) -> LineCtor {
7122 LineCtor {
7123 start: Point2d {
7124 x: Expr::Number(Number { value: start_x, units }),
7125 y: Expr::Number(Number { value: start_y, units }),
7126 },
7127 end: Point2d {
7128 x: Expr::Number(Number { value: end_x, units }),
7129 y: Expr::Number(Number { value: end_y, units }),
7130 },
7131 construction: None,
7132 }
7133 }
7134
7135 async fn create_sketch_with_single_line(
7136 frontend: &mut FrontendState,
7137 ctx: &ExecutorContext,
7138 mock_ctx: &ExecutorContext,
7139 version: Version,
7140 ) -> (ObjectId, ObjectId, SourceDelta, SceneGraphDelta) {
7141 frontend.program = Program::empty();
7142
7143 let sketch_args = SketchCtor {
7144 on: Plane::Default(PlaneName::Xy),
7145 };
7146 let (_src_delta, _scene_delta, sketch_id) = frontend
7147 .new_sketch(ctx, ProjectId(0), FileId(0), version, sketch_args)
7148 .await
7149 .unwrap();
7150
7151 let segment = SegmentCtor::Line(make_line_ctor(0.0, 0.0, 10.0, 10.0, NumericSuffix::Mm));
7152 let (source_delta, scene_graph_delta) = frontend
7153 .add_segment(mock_ctx, version, sketch_id, segment, None)
7154 .await
7155 .unwrap();
7156 let line_id = *scene_graph_delta
7157 .new_objects
7158 .last()
7159 .expect("Expected line object id to be created");
7160
7161 (sketch_id, line_id, source_delta, scene_graph_delta)
7162 }
7163
7164 async fn seed_frontend_with_mock(frontend: &mut FrontendState, mock_ctx: &ExecutorContext, program: &Program) {
7165 frontend.program = program.clone();
7166 let outcome = mock_ctx.run_mock(program, &MockConfig::default()).await.unwrap();
7167 frontend.update_state_after_exec(outcome, true);
7168 }
7169
7170 #[tokio::test(flavor = "multi_thread")]
7171 async fn test_sketch_checkpoint_round_trip_restores_state() {
7172 let mut frontend = FrontendState::new();
7173 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7174 let mock_ctx = ExecutorContext::new_mock(None).await;
7175 let version = Version(0);
7176
7177 let (sketch_id, line_id, source_delta, scene_graph_delta) =
7178 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7179
7180 let expected_source = source_delta.text.clone();
7181 let expected_scene_graph = frontend.scene_graph.clone();
7182 let expected_exec_outcome = scene_graph_delta.exec_outcome.clone();
7183 let expected_point_freedom_cache = frontend.point_freedom_cache.clone();
7184
7185 let checkpoint_id = frontend
7186 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7187 .await
7188 .unwrap();
7189
7190 let edited_segments = vec![ExistingSegmentCtor {
7191 id: line_id,
7192 ctor: SegmentCtor::Line(make_line_ctor(1.0, 2.0, 13.0, 14.0, NumericSuffix::Mm)),
7193 }];
7194 let (edited_source, _edited_scene) = frontend
7195 .edit_segments(&mock_ctx, version, sketch_id, edited_segments)
7196 .await
7197 .unwrap();
7198 assert_ne!(edited_source.text, expected_source);
7199
7200 let restored = frontend.restore_sketch_checkpoint(checkpoint_id).await.unwrap();
7201
7202 assert_eq!(restored.source_delta.text, expected_source);
7203 assert_eq!(restored.scene_graph_delta.new_graph, expected_scene_graph);
7204 assert!(restored.scene_graph_delta.invalidates_ids);
7205 assert_eq!(restored.scene_graph_delta.exec_outcome, expected_exec_outcome);
7206 assert_eq!(frontend.scene_graph, expected_scene_graph);
7207 assert_eq!(frontend.point_freedom_cache, expected_point_freedom_cache);
7208
7209 ctx.close().await;
7210 }
7211
7212 #[tokio::test(flavor = "multi_thread")]
7213 async fn test_sketch_checkpoints_prune_oldest_entries() {
7214 let mut frontend = FrontendState::new();
7215 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7216 let mock_ctx = ExecutorContext::new_mock(None).await;
7217 let version = Version(0);
7218
7219 let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
7220 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7221
7222 let mut checkpoint_ids = Vec::new();
7223 for _ in 0..(MAX_SKETCH_CHECKPOINTS + 3) {
7224 checkpoint_ids.push(
7225 frontend
7226 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7227 .await
7228 .unwrap(),
7229 );
7230 }
7231
7232 assert_eq!(frontend.sketch_checkpoints.len(), MAX_SKETCH_CHECKPOINTS);
7233 assert!(checkpoint_ids.windows(2).all(|ids| ids[0] < ids[1]));
7234
7235 let oldest_retained = checkpoint_ids[3];
7236 assert_eq!(
7237 frontend.sketch_checkpoints.front().map(|checkpoint| checkpoint.id),
7238 Some(oldest_retained)
7239 );
7240
7241 let evicted_restore = frontend.restore_sketch_checkpoint(checkpoint_ids[0]).await;
7242 assert!(evicted_restore.is_err());
7243 assert!(evicted_restore.unwrap_err().msg.contains("Sketch checkpoint not found"));
7244
7245 frontend
7246 .restore_sketch_checkpoint(*checkpoint_ids.last().unwrap())
7247 .await
7248 .unwrap();
7249
7250 ctx.close().await;
7251 }
7252
7253 #[tokio::test(flavor = "multi_thread")]
7254 async fn test_restore_sketch_checkpoint_missing_id_returns_error() {
7255 let mut frontend = FrontendState::new();
7256 let missing_checkpoint = SketchCheckpointId::new(999);
7257
7258 let err = frontend
7259 .restore_sketch_checkpoint(missing_checkpoint)
7260 .await
7261 .expect_err("Expected restore to fail for missing checkpoint");
7262
7263 assert!(err.msg.contains("Sketch checkpoint not found"));
7264 }
7265
7266 #[tokio::test(flavor = "multi_thread")]
7267 async fn test_clear_sketch_checkpoints_removes_all_restore_points() {
7268 let mut frontend = FrontendState::new();
7269 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7270 let mock_ctx = ExecutorContext::new_mock(None).await;
7271 let version = Version(0);
7272
7273 let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
7274 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7275
7276 let checkpoint_a = frontend
7277 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7278 .await
7279 .unwrap();
7280 let checkpoint_b = frontend
7281 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7282 .await
7283 .unwrap();
7284 assert_eq!(frontend.sketch_checkpoints.len(), 2);
7285
7286 frontend.clear_sketch_checkpoints();
7287 assert!(frontend.sketch_checkpoints.is_empty());
7288 frontend.restore_sketch_checkpoint(checkpoint_a).await.unwrap_err();
7289 frontend.restore_sketch_checkpoint(checkpoint_b).await.unwrap_err();
7290
7291 ctx.close().await;
7292 }
7293
7294 #[tokio::test(flavor = "multi_thread")]
7295 async fn test_hack_set_program_keeps_old_checkpoints_and_adds_fresh_baseline() {
7296 let mut frontend = FrontendState::new();
7297 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7298 let mock_ctx = ExecutorContext::new_mock(None).await;
7299 let version = Version(0);
7300
7301 let (_sketch_id, _line_id, source_delta, scene_graph_delta) =
7302 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7303 let old_source = source_delta.text.clone();
7304 let old_checkpoint = frontend
7305 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7306 .await
7307 .unwrap();
7308 let initial_checkpoint_count = frontend.sketch_checkpoints.len();
7309
7310 let new_program = Program::parse("sketch(on = XY) {\n point(at = [1mm, 2mm])\n}\n")
7311 .unwrap()
7312 .0
7313 .unwrap();
7314
7315 let result = frontend.hack_set_program(&ctx, new_program).await.unwrap();
7316 let SetProgramOutcome::Success {
7317 checkpoint_id: Some(new_checkpoint),
7318 ..
7319 } = result
7320 else {
7321 panic!("Expected Success with a fresh checkpoint baseline");
7322 };
7323
7324 assert_eq!(frontend.sketch_checkpoints.len(), initial_checkpoint_count + 1);
7325
7326 let old_restore = frontend.restore_sketch_checkpoint(old_checkpoint).await.unwrap();
7327 assert_eq!(old_restore.source_delta.text, old_source);
7328
7329 let new_restore = frontend.restore_sketch_checkpoint(new_checkpoint).await.unwrap();
7330 assert!(new_restore.source_delta.text.contains("point(at = [1mm, 2mm])"));
7331
7332 ctx.close().await;
7333 }
7334
7335 #[tokio::test(flavor = "multi_thread")]
7336 async fn test_hack_set_program_exec_failure_does_not_add_checkpoint() {
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 let old_checkpoint = frontend
7345 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7346 .await
7347 .unwrap();
7348 let checkpoint_count_before = frontend.sketch_checkpoints.len();
7349
7350 let failing_program = Program::parse(
7351 "sketch(on = XY) {\n line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])\n}\n\nbad = missing_name\n",
7352 )
7353 .unwrap()
7354 .0
7355 .unwrap();
7356
7357 let result = frontend.hack_set_program(&ctx, failing_program).await.unwrap();
7358 assert!(matches!(result, SetProgramOutcome::ExecFailure { .. }));
7359 assert_eq!(frontend.sketch_checkpoints.len(), checkpoint_count_before);
7360 frontend.restore_sketch_checkpoint(old_checkpoint).await.unwrap();
7361
7362 ctx.close().await;
7363 }
7364
7365 #[tokio::test(flavor = "multi_thread")]
7366 async fn test_restore_sketch_checkpoint_restores_and_clears_mock_memory() {
7367 let mut frontend = FrontendState::new();
7368 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7369
7370 let program = Program::parse(
7371 "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",
7372 )
7373 .unwrap()
7374 .0
7375 .unwrap();
7376 let set_program_outcome = frontend.hack_set_program(&ctx, program).await.unwrap();
7377 let SetProgramOutcome::Success { exec_outcome, .. } = set_program_outcome else {
7378 panic!("Expected successful baseline program execution");
7379 };
7380
7381 clear_mem_cache().await;
7382 assert!(read_old_memory().await.is_none());
7383
7384 let checkpoint_without_mock_memory = frontend
7385 .create_sketch_checkpoint((*exec_outcome).clone())
7386 .await
7387 .unwrap();
7388
7389 write_old_memory(SketchModeState::new_for_tests()).await;
7390 assert!(read_old_memory().await.is_some());
7391
7392 let checkpoint_with_mock_memory = frontend
7393 .create_sketch_checkpoint((*exec_outcome).clone())
7394 .await
7395 .unwrap();
7396
7397 clear_mem_cache().await;
7398 assert!(read_old_memory().await.is_none());
7399
7400 frontend
7401 .restore_sketch_checkpoint(checkpoint_with_mock_memory)
7402 .await
7403 .unwrap();
7404 assert!(read_old_memory().await.is_some());
7405
7406 frontend
7407 .restore_sketch_checkpoint(checkpoint_without_mock_memory)
7408 .await
7409 .unwrap();
7410 assert!(read_old_memory().await.is_none());
7411
7412 ctx.close().await;
7413 }
7414
7415 #[tokio::test(flavor = "multi_thread")]
7416 async fn test_hack_set_program_exec_error_still_allows_edit_sketch() {
7417 let source = "\
7418sketch(on = XY) {
7419 line1 = line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])
7420}
7421
7422bad = missing_name
7423";
7424 let program = Program::parse(source).unwrap().0.unwrap();
7425
7426 let mut frontend = FrontendState::new();
7427
7428 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7429 let mock_ctx = ExecutorContext::new_mock(None).await;
7430 let version = Version(0);
7431 let project_id = ProjectId(0);
7432 let file_id = FileId(0);
7433
7434 let SetProgramOutcome::ExecFailure { .. } = frontend.hack_set_program(&ctx, program).await.unwrap() else {
7435 panic!("Expected ExecFailure from hack_set_program due to syntax error in program");
7436 };
7437
7438 let sketch_id = frontend
7439 .scene_graph
7440 .objects
7441 .iter()
7442 .find_map(|obj| matches!(obj.kind, ObjectKind::Sketch(_)).then_some(obj.id))
7443 .expect("Expected sketch object from errored hack_set_program");
7444
7445 frontend
7446 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
7447 .await
7448 .unwrap();
7449
7450 ctx.close().await;
7451 mock_ctx.close().await;
7452 }
7453
7454 #[tokio::test(flavor = "multi_thread")]
7455 async fn test_new_sketch_add_point_edit_point() {
7456 let program = Program::empty();
7457
7458 let mut frontend = FrontendState::new();
7459 frontend.program = program;
7460
7461 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7462 let mock_ctx = ExecutorContext::new_mock(None).await;
7463 let version = Version(0);
7464
7465 let sketch_args = SketchCtor {
7466 on: Plane::Default(PlaneName::Xy),
7467 };
7468 let (_src_delta, scene_delta, sketch_id) = frontend
7469 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
7470 .await
7471 .unwrap();
7472 assert_eq!(sketch_id, ObjectId(1));
7473 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
7474 let sketch_object = &scene_delta.new_graph.objects[1];
7475 assert_eq!(sketch_object.id, ObjectId(1));
7476 assert_eq!(
7477 sketch_object.kind,
7478 ObjectKind::Sketch(Sketch {
7479 args: SketchCtor {
7480 on: Plane::Default(PlaneName::Xy)
7481 },
7482 plane: ObjectId(0),
7483 segments: vec![],
7484 constraints: vec![],
7485 })
7486 );
7487 assert_eq!(scene_delta.new_graph.objects.len(), 2);
7488
7489 let point_ctor = PointCtor {
7490 position: Point2d {
7491 x: Expr::Number(Number {
7492 value: 1.0,
7493 units: NumericSuffix::Inch,
7494 }),
7495 y: Expr::Number(Number {
7496 value: 2.0,
7497 units: NumericSuffix::Inch,
7498 }),
7499 },
7500 };
7501 let segment = SegmentCtor::Point(point_ctor);
7502 let (src_delta, scene_delta) = frontend
7503 .add_segment(&mock_ctx, version, sketch_id, segment, None)
7504 .await
7505 .unwrap();
7506 assert_eq!(
7507 src_delta.text.as_str(),
7508 "sketch001 = sketch(on = XY) {
7509 point(at = [1in, 2in])
7510}
7511"
7512 );
7513 assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
7514 assert_eq!(scene_delta.new_graph.objects.len(), 3);
7515 for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
7516 assert_eq!(scene_object.id.0, i);
7517 }
7518
7519 let point_id = *scene_delta.new_objects.last().unwrap();
7520
7521 let point_ctor = PointCtor {
7522 position: Point2d {
7523 x: Expr::Number(Number {
7524 value: 3.0,
7525 units: NumericSuffix::Inch,
7526 }),
7527 y: Expr::Number(Number {
7528 value: 4.0,
7529 units: NumericSuffix::Inch,
7530 }),
7531 },
7532 };
7533 let segments = vec![ExistingSegmentCtor {
7534 id: point_id,
7535 ctor: SegmentCtor::Point(point_ctor),
7536 }];
7537 let (src_delta, scene_delta) = frontend
7538 .edit_segments(&mock_ctx, version, sketch_id, segments)
7539 .await
7540 .unwrap();
7541 assert_eq!(
7542 src_delta.text.as_str(),
7543 "sketch001 = sketch(on = XY) {
7544 point(at = [3in, 4in])
7545}
7546"
7547 );
7548 assert_eq!(scene_delta.new_objects, vec![]);
7549 assert_eq!(scene_delta.new_graph.objects.len(), 3);
7550
7551 ctx.close().await;
7552 mock_ctx.close().await;
7553 }
7554
7555 #[tokio::test(flavor = "multi_thread")]
7556 async fn test_new_sketch_add_line_edit_line() {
7557 let program = Program::empty();
7558
7559 let mut frontend = FrontendState::new();
7560 frontend.program = program;
7561
7562 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7563 let mock_ctx = ExecutorContext::new_mock(None).await;
7564 let version = Version(0);
7565
7566 let sketch_args = SketchCtor {
7567 on: Plane::Default(PlaneName::Xy),
7568 };
7569 let (_src_delta, scene_delta, sketch_id) = frontend
7570 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
7571 .await
7572 .unwrap();
7573 assert_eq!(sketch_id, ObjectId(1));
7574 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
7575 let sketch_object = &scene_delta.new_graph.objects[1];
7576 assert_eq!(sketch_object.id, ObjectId(1));
7577 assert_eq!(
7578 sketch_object.kind,
7579 ObjectKind::Sketch(Sketch {
7580 args: SketchCtor {
7581 on: Plane::Default(PlaneName::Xy)
7582 },
7583 plane: ObjectId(0),
7584 segments: vec![],
7585 constraints: vec![],
7586 })
7587 );
7588 assert_eq!(scene_delta.new_graph.objects.len(), 2);
7589
7590 let line_ctor = LineCtor {
7591 start: Point2d {
7592 x: Expr::Number(Number {
7593 value: 0.0,
7594 units: NumericSuffix::Mm,
7595 }),
7596 y: Expr::Number(Number {
7597 value: 0.0,
7598 units: NumericSuffix::Mm,
7599 }),
7600 },
7601 end: Point2d {
7602 x: Expr::Number(Number {
7603 value: 10.0,
7604 units: NumericSuffix::Mm,
7605 }),
7606 y: Expr::Number(Number {
7607 value: 10.0,
7608 units: NumericSuffix::Mm,
7609 }),
7610 },
7611 construction: None,
7612 };
7613 let segment = SegmentCtor::Line(line_ctor);
7614 let (src_delta, scene_delta) = frontend
7615 .add_segment(&mock_ctx, version, sketch_id, segment, None)
7616 .await
7617 .unwrap();
7618 assert_eq!(
7619 src_delta.text.as_str(),
7620 "sketch001 = sketch(on = XY) {
7621 line(start = [0mm, 0mm], end = [10mm, 10mm])
7622}
7623"
7624 );
7625 assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
7626 assert_eq!(scene_delta.new_graph.objects.len(), 5);
7627 for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
7628 assert_eq!(scene_object.id.0, i);
7629 }
7630
7631 let line = *scene_delta.new_objects.last().unwrap();
7633
7634 let line_ctor = LineCtor {
7635 start: Point2d {
7636 x: Expr::Number(Number {
7637 value: 1.0,
7638 units: NumericSuffix::Mm,
7639 }),
7640 y: Expr::Number(Number {
7641 value: 2.0,
7642 units: NumericSuffix::Mm,
7643 }),
7644 },
7645 end: Point2d {
7646 x: Expr::Number(Number {
7647 value: 13.0,
7648 units: NumericSuffix::Mm,
7649 }),
7650 y: Expr::Number(Number {
7651 value: 14.0,
7652 units: NumericSuffix::Mm,
7653 }),
7654 },
7655 construction: None,
7656 };
7657 let segments = vec![ExistingSegmentCtor {
7658 id: line,
7659 ctor: SegmentCtor::Line(line_ctor),
7660 }];
7661 let (src_delta, scene_delta) = frontend
7662 .edit_segments(&mock_ctx, version, sketch_id, segments)
7663 .await
7664 .unwrap();
7665 assert_eq!(
7666 src_delta.text.as_str(),
7667 "sketch001 = sketch(on = XY) {
7668 line(start = [1mm, 2mm], end = [13mm, 14mm])
7669}
7670"
7671 );
7672 assert_eq!(scene_delta.new_objects, vec![]);
7673 assert_eq!(scene_delta.new_graph.objects.len(), 5);
7674
7675 ctx.close().await;
7676 mock_ctx.close().await;
7677 }
7678
7679 #[tokio::test(flavor = "multi_thread")]
7680 async fn test_new_sketch_add_arc_edit_arc() {
7681 let program = Program::empty();
7682
7683 let mut frontend = FrontendState::new();
7684 frontend.program = program;
7685
7686 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7687 let mock_ctx = ExecutorContext::new_mock(None).await;
7688 let version = Version(0);
7689
7690 let sketch_args = SketchCtor {
7691 on: Plane::Default(PlaneName::Xy),
7692 };
7693 let (_src_delta, scene_delta, sketch_id) = frontend
7694 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
7695 .await
7696 .unwrap();
7697 assert_eq!(sketch_id, ObjectId(1));
7698 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
7699 let sketch_object = &scene_delta.new_graph.objects[1];
7700 assert_eq!(sketch_object.id, ObjectId(1));
7701 assert_eq!(
7702 sketch_object.kind,
7703 ObjectKind::Sketch(Sketch {
7704 args: SketchCtor {
7705 on: Plane::Default(PlaneName::Xy),
7706 },
7707 plane: ObjectId(0),
7708 segments: vec![],
7709 constraints: vec![],
7710 })
7711 );
7712 assert_eq!(scene_delta.new_graph.objects.len(), 2);
7713
7714 let arc_ctor = ArcCtor {
7715 start: Point2d {
7716 x: Expr::Var(Number {
7717 value: 0.0,
7718 units: NumericSuffix::Mm,
7719 }),
7720 y: Expr::Var(Number {
7721 value: 0.0,
7722 units: NumericSuffix::Mm,
7723 }),
7724 },
7725 end: Point2d {
7726 x: Expr::Var(Number {
7727 value: 10.0,
7728 units: NumericSuffix::Mm,
7729 }),
7730 y: Expr::Var(Number {
7731 value: 10.0,
7732 units: NumericSuffix::Mm,
7733 }),
7734 },
7735 center: Point2d {
7736 x: Expr::Var(Number {
7737 value: 10.0,
7738 units: NumericSuffix::Mm,
7739 }),
7740 y: Expr::Var(Number {
7741 value: 0.0,
7742 units: NumericSuffix::Mm,
7743 }),
7744 },
7745 construction: None,
7746 };
7747 let segment = SegmentCtor::Arc(arc_ctor);
7748 let (src_delta, scene_delta) = frontend
7749 .add_segment(&mock_ctx, version, sketch_id, segment, None)
7750 .await
7751 .unwrap();
7752 assert_eq!(
7753 src_delta.text.as_str(),
7754 "sketch001 = sketch(on = XY) {
7755 arc(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm], center = [var 10mm, var 0mm])
7756}
7757"
7758 );
7759 assert_eq!(
7760 scene_delta.new_objects,
7761 vec![ObjectId(2), ObjectId(3), ObjectId(4), ObjectId(5)]
7762 );
7763 for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
7764 assert_eq!(scene_object.id.0, i);
7765 }
7766 assert_eq!(scene_delta.new_graph.objects.len(), 6);
7767
7768 let arc = *scene_delta.new_objects.last().unwrap();
7770
7771 let arc_ctor = ArcCtor {
7772 start: Point2d {
7773 x: Expr::Var(Number {
7774 value: 1.0,
7775 units: NumericSuffix::Mm,
7776 }),
7777 y: Expr::Var(Number {
7778 value: 2.0,
7779 units: NumericSuffix::Mm,
7780 }),
7781 },
7782 end: Point2d {
7783 x: Expr::Var(Number {
7784 value: 13.0,
7785 units: NumericSuffix::Mm,
7786 }),
7787 y: Expr::Var(Number {
7788 value: 14.0,
7789 units: NumericSuffix::Mm,
7790 }),
7791 },
7792 center: Point2d {
7793 x: Expr::Var(Number {
7794 value: 13.0,
7795 units: NumericSuffix::Mm,
7796 }),
7797 y: Expr::Var(Number {
7798 value: 2.0,
7799 units: NumericSuffix::Mm,
7800 }),
7801 },
7802 construction: None,
7803 };
7804 let segments = vec![ExistingSegmentCtor {
7805 id: arc,
7806 ctor: SegmentCtor::Arc(arc_ctor),
7807 }];
7808 let (src_delta, scene_delta) = frontend
7809 .edit_segments(&mock_ctx, version, sketch_id, segments)
7810 .await
7811 .unwrap();
7812 assert_eq!(
7813 src_delta.text.as_str(),
7814 "sketch001 = sketch(on = XY) {
7815 arc(start = [var 1mm, var 2mm], end = [var 13mm, var 14mm], center = [var 13mm, var 2mm])
7816}
7817"
7818 );
7819 assert_eq!(scene_delta.new_objects, vec![]);
7820 assert_eq!(scene_delta.new_graph.objects.len(), 6);
7821
7822 ctx.close().await;
7823 mock_ctx.close().await;
7824 }
7825
7826 #[tokio::test(flavor = "multi_thread")]
7827 async fn test_new_sketch_add_circle_edit_circle() {
7828 let program = Program::empty();
7829
7830 let mut frontend = FrontendState::new();
7831 frontend.program = program;
7832
7833 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7834 let mock_ctx = ExecutorContext::new_mock(None).await;
7835 let version = Version(0);
7836
7837 let sketch_args = SketchCtor {
7838 on: Plane::Default(PlaneName::Xy),
7839 };
7840 let (_src_delta, _scene_delta, sketch_id) = frontend
7841 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
7842 .await
7843 .unwrap();
7844
7845 let circle_ctor = CircleCtor {
7847 start: Point2d {
7848 x: Expr::Var(Number {
7849 value: 5.0,
7850 units: NumericSuffix::Mm,
7851 }),
7852 y: Expr::Var(Number {
7853 value: 0.0,
7854 units: NumericSuffix::Mm,
7855 }),
7856 },
7857 center: Point2d {
7858 x: Expr::Var(Number {
7859 value: 0.0,
7860 units: NumericSuffix::Mm,
7861 }),
7862 y: Expr::Var(Number {
7863 value: 0.0,
7864 units: NumericSuffix::Mm,
7865 }),
7866 },
7867 construction: None,
7868 };
7869 let segment = SegmentCtor::Circle(circle_ctor);
7870 let (src_delta, scene_delta) = frontend
7871 .add_segment(&mock_ctx, version, sketch_id, segment, None)
7872 .await
7873 .unwrap();
7874 assert_eq!(
7875 src_delta.text.as_str(),
7876 "sketch001 = sketch(on = XY) {
7877 circle1 = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
7878}
7879"
7880 );
7881 assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
7883 assert_eq!(scene_delta.new_graph.objects.len(), 5);
7884
7885 let circle = *scene_delta.new_objects.last().unwrap();
7886
7887 let circle_ctor = CircleCtor {
7889 start: Point2d {
7890 x: Expr::Var(Number {
7891 value: 10.0,
7892 units: NumericSuffix::Mm,
7893 }),
7894 y: Expr::Var(Number {
7895 value: 0.0,
7896 units: NumericSuffix::Mm,
7897 }),
7898 },
7899 center: Point2d {
7900 x: Expr::Var(Number {
7901 value: 3.0,
7902 units: NumericSuffix::Mm,
7903 }),
7904 y: Expr::Var(Number {
7905 value: 4.0,
7906 units: NumericSuffix::Mm,
7907 }),
7908 },
7909 construction: None,
7910 };
7911 let segments = vec![ExistingSegmentCtor {
7912 id: circle,
7913 ctor: SegmentCtor::Circle(circle_ctor),
7914 }];
7915 let (src_delta, scene_delta) = frontend
7916 .edit_segments(&mock_ctx, version, sketch_id, segments)
7917 .await
7918 .unwrap();
7919 assert_eq!(
7920 src_delta.text.as_str(),
7921 "sketch001 = sketch(on = XY) {
7922 circle1 = circle(start = [var 10mm, var 0mm], center = [var 3mm, var 4mm])
7923}
7924"
7925 );
7926 assert_eq!(scene_delta.new_objects, vec![]);
7927 assert_eq!(scene_delta.new_graph.objects.len(), 5);
7928
7929 ctx.close().await;
7930 mock_ctx.close().await;
7931 }
7932
7933 #[tokio::test(flavor = "multi_thread")]
7934 async fn test_delete_circle() {
7935 let initial_source = "sketch001 = sketch(on = XY) {
7936 circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
7937}
7938";
7939
7940 let program = Program::parse(initial_source).unwrap().0.unwrap();
7941 let mut frontend = FrontendState::new();
7942
7943 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7944 let mock_ctx = ExecutorContext::new_mock(None).await;
7945 let version = Version(0);
7946
7947 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
7948 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7949 let sketch_id = sketch_object.id;
7950 let sketch = expect_sketch(sketch_object);
7951
7952 assert_eq!(sketch.segments.len(), 3);
7954 let circle_id = sketch.segments[2];
7955
7956 let (src_delta, scene_delta) = frontend
7958 .delete_objects(&mock_ctx, version, sketch_id, vec![], vec![circle_id])
7959 .await
7960 .unwrap();
7961 assert_eq!(
7962 src_delta.text.as_str(),
7963 "sketch001 = sketch(on = XY) {
7964}
7965"
7966 );
7967 let new_sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
7968 let new_sketch = expect_sketch(new_sketch_object);
7969 assert_eq!(new_sketch.segments.len(), 0);
7970
7971 ctx.close().await;
7972 mock_ctx.close().await;
7973 }
7974
7975 #[tokio::test(flavor = "multi_thread")]
7976 async fn test_edit_circle_via_point() {
7977 let initial_source = "sketch001 = sketch(on = XY) {
7978 circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
7979}
7980";
7981
7982 let program = Program::parse(initial_source).unwrap().0.unwrap();
7983 let mut frontend = FrontendState::new();
7984
7985 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7986 let mock_ctx = ExecutorContext::new_mock(None).await;
7987 let version = Version(0);
7988
7989 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
7990 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7991 let sketch_id = sketch_object.id;
7992 let sketch = expect_sketch(sketch_object);
7993
7994 let circle_id = sketch
7996 .segments
7997 .iter()
7998 .copied()
7999 .find(|seg_id| {
8000 matches!(
8001 &frontend.scene_graph.objects[seg_id.0].kind,
8002 ObjectKind::Segment {
8003 segment: Segment::Circle(_)
8004 }
8005 )
8006 })
8007 .expect("Expected a circle segment in sketch");
8008 let circle_object = &frontend.scene_graph.objects[circle_id.0];
8009 let ObjectKind::Segment {
8010 segment: Segment::Circle(circle),
8011 } = &circle_object.kind
8012 else {
8013 panic!("Expected circle segment, got: {:?}", circle_object.kind);
8014 };
8015 let start_point_id = circle.start;
8016
8017 let segments = vec![ExistingSegmentCtor {
8019 id: start_point_id,
8020 ctor: SegmentCtor::Point(PointCtor {
8021 position: Point2d {
8022 x: Expr::Var(Number {
8023 value: 7.0,
8024 units: NumericSuffix::Mm,
8025 }),
8026 y: Expr::Var(Number {
8027 value: 1.0,
8028 units: NumericSuffix::Mm,
8029 }),
8030 },
8031 }),
8032 }];
8033 let (src_delta, _scene_delta) = frontend
8034 .edit_segments(&mock_ctx, version, sketch_id, segments)
8035 .await
8036 .unwrap();
8037 assert_eq!(
8038 src_delta.text.as_str(),
8039 "sketch001 = sketch(on = XY) {
8040 circle(start = [var 7mm, var 1mm], center = [var 0mm, var 0mm])
8041}
8042"
8043 );
8044
8045 ctx.close().await;
8046 mock_ctx.close().await;
8047 }
8048
8049 #[tokio::test(flavor = "multi_thread")]
8050 async fn test_add_line_when_sketch_block_uses_variable() {
8051 let initial_source = "s = sketch(on = XY) {}
8052";
8053
8054 let program = Program::parse(initial_source).unwrap().0.unwrap();
8055
8056 let mut frontend = FrontendState::new();
8057
8058 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8059 let mock_ctx = ExecutorContext::new_mock(None).await;
8060 let version = Version(0);
8061
8062 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8063 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8064 let sketch_id = sketch_object.id;
8065
8066 let line_ctor = LineCtor {
8067 start: Point2d {
8068 x: Expr::Number(Number {
8069 value: 0.0,
8070 units: NumericSuffix::Mm,
8071 }),
8072 y: Expr::Number(Number {
8073 value: 0.0,
8074 units: NumericSuffix::Mm,
8075 }),
8076 },
8077 end: Point2d {
8078 x: Expr::Number(Number {
8079 value: 10.0,
8080 units: NumericSuffix::Mm,
8081 }),
8082 y: Expr::Number(Number {
8083 value: 10.0,
8084 units: NumericSuffix::Mm,
8085 }),
8086 },
8087 construction: None,
8088 };
8089 let segment = SegmentCtor::Line(line_ctor);
8090 let (src_delta, scene_delta) = frontend
8091 .add_segment(&mock_ctx, version, sketch_id, segment, None)
8092 .await
8093 .unwrap();
8094 assert_eq!(
8095 src_delta.text.as_str(),
8096 "s = sketch(on = XY) {
8097 line(start = [0mm, 0mm], end = [10mm, 10mm])
8098}
8099"
8100 );
8101 assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
8102 assert_eq!(scene_delta.new_graph.objects.len(), 5);
8103
8104 ctx.close().await;
8105 mock_ctx.close().await;
8106 }
8107
8108 #[tokio::test(flavor = "multi_thread")]
8109 async fn test_new_sketch_add_line_delete_sketch() {
8110 let program = Program::empty();
8111
8112 let mut frontend = FrontendState::new();
8113 frontend.program = program;
8114
8115 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8116 let mock_ctx = ExecutorContext::new_mock(None).await;
8117 let version = Version(0);
8118
8119 let sketch_args = SketchCtor {
8120 on: Plane::Default(PlaneName::Xy),
8121 };
8122 let (_src_delta, scene_delta, sketch_id) = frontend
8123 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
8124 .await
8125 .unwrap();
8126 assert_eq!(sketch_id, ObjectId(1));
8127 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
8128 let sketch_object = &scene_delta.new_graph.objects[1];
8129 assert_eq!(sketch_object.id, ObjectId(1));
8130 assert_eq!(
8131 sketch_object.kind,
8132 ObjectKind::Sketch(Sketch {
8133 args: SketchCtor {
8134 on: Plane::Default(PlaneName::Xy)
8135 },
8136 plane: ObjectId(0),
8137 segments: vec![],
8138 constraints: vec![],
8139 })
8140 );
8141 assert_eq!(scene_delta.new_graph.objects.len(), 2);
8142
8143 let line_ctor = LineCtor {
8144 start: Point2d {
8145 x: Expr::Number(Number {
8146 value: 0.0,
8147 units: NumericSuffix::Mm,
8148 }),
8149 y: Expr::Number(Number {
8150 value: 0.0,
8151 units: NumericSuffix::Mm,
8152 }),
8153 },
8154 end: Point2d {
8155 x: Expr::Number(Number {
8156 value: 10.0,
8157 units: NumericSuffix::Mm,
8158 }),
8159 y: Expr::Number(Number {
8160 value: 10.0,
8161 units: NumericSuffix::Mm,
8162 }),
8163 },
8164 construction: None,
8165 };
8166 let segment = SegmentCtor::Line(line_ctor);
8167 let (src_delta, scene_delta) = frontend
8168 .add_segment(&mock_ctx, version, sketch_id, segment, None)
8169 .await
8170 .unwrap();
8171 assert_eq!(
8172 src_delta.text.as_str(),
8173 "sketch001 = sketch(on = XY) {
8174 line(start = [0mm, 0mm], end = [10mm, 10mm])
8175}
8176"
8177 );
8178 assert_eq!(scene_delta.new_graph.objects.len(), 5);
8179
8180 let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8181 assert_eq!(src_delta.text.as_str(), "");
8182 assert_eq!(scene_delta.new_graph.objects.len(), 0);
8183
8184 ctx.close().await;
8185 mock_ctx.close().await;
8186 }
8187
8188 #[tokio::test(flavor = "multi_thread")]
8189 async fn test_delete_sketch_when_sketch_block_uses_variable() {
8190 let initial_source = "s = sketch(on = XY) {}
8191";
8192
8193 let program = Program::parse(initial_source).unwrap().0.unwrap();
8194
8195 let mut frontend = FrontendState::new();
8196
8197 let ctx = ExecutorContext::new_with_engine(sync::Arc::new(EngineManager::new_mock()), Default::default());
8198 let version = Version(0);
8199
8200 frontend.hack_set_program(&ctx, program).await.unwrap();
8201 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8202 let sketch_id = sketch_object.id;
8203
8204 let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8205 assert_eq!(src_delta.text.as_str(), "");
8206 assert_eq!(scene_delta.new_graph.objects.len(), 0);
8207
8208 ctx.close().await;
8209 }
8210
8211 #[tokio::test(flavor = "multi_thread")]
8212 async fn test_delete_sketch_after_comment() {
8213 let initial_source = "sketch001 = sketch(on = XZ) {
8214}
8215";
8216
8217 let program = Program::parse(initial_source).unwrap().0.unwrap();
8218 let mut frontend = FrontendState::new();
8219
8220 let ctx = ExecutorContext::new_with_engine(sync::Arc::new(EngineManager::new_mock()), Default::default());
8221 let version = Version(0);
8222
8223 frontend.hack_set_program(&ctx, program).await.unwrap();
8224 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8225 let sketch_id = sketch_object.id;
8226 let original_source = sketch_object.source.clone();
8227
8228 let commented_source = "// test 1
8229sketch001 = sketch(on = XZ) {
8230}
8231";
8232 let commented_program = Program::parse(commented_source).unwrap().0.unwrap();
8233 frontend.engine_execute(&ctx, commented_program).await.unwrap();
8234
8235 let cached_sketch_object = &frontend.scene_graph.objects[sketch_id.0];
8236 assert_eq!(cached_sketch_object.source, original_source);
8237
8238 let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8239 assert!(
8240 !src_delta.text.contains("sketch001"),
8241 "sketch was not deleted: {}",
8242 src_delta.text
8243 );
8244 assert_eq!(src_delta.text.as_str(), "// test 1\n");
8246 assert_eq!(scene_delta.new_graph.objects.len(), 0);
8247
8248 ctx.close().await;
8249 }
8250
8251 #[tokio::test(flavor = "multi_thread")]
8252 async fn test_delete_sketch_preserves_pre_comment_when_followed_by_code() {
8253 let initial_source = "sketch001 = sketch(on = XZ) {
8254}
8255foo = 1
8256";
8257
8258 let program = Program::parse(initial_source).unwrap().0.unwrap();
8259 let mut frontend = FrontendState::new();
8260
8261 let ctx = ExecutorContext::new_with_engine(sync::Arc::new(EngineManager::new_mock()), Default::default());
8262 let version = Version(0);
8263
8264 frontend.hack_set_program(&ctx, program).await.unwrap();
8265 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8266 let sketch_id = sketch_object.id;
8267
8268 let commented_source = "// keep me
8269sketch001 = sketch(on = XZ) {
8270}
8271foo = 1
8272";
8273 let commented_program = Program::parse(commented_source).unwrap().0.unwrap();
8274 frontend.engine_execute(&ctx, commented_program).await.unwrap();
8275
8276 let (src_delta, _scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8277 assert_eq!(src_delta.text.as_str(), "// keep me\nfoo = 1\n");
8279
8280 ctx.close().await;
8281 }
8282
8283 #[tokio::test(flavor = "multi_thread")]
8284 async fn test_delete_segment_preserves_pre_comment() {
8285 let initial_source = "\
8286sketch(on = XY) {
8287 point(at = [var 1, var 2])
8288 // describe the middle point
8289 point(at = [var 3, var 4])
8290 point(at = [var 5, var 6])
8291}
8292";
8293
8294 let program = Program::parse(initial_source).unwrap().0.unwrap();
8295 let mut frontend = FrontendState::new();
8296
8297 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8298 let mock_ctx = ExecutorContext::new_mock(None).await;
8299 let version = Version(0);
8300
8301 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8302 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8303 let sketch_id = sketch_object.id;
8304 let sketch = expect_sketch(sketch_object);
8305
8306 let middle_point_id = *sketch.segments.get(1).unwrap();
8307
8308 let (src_delta, _scene_delta) = frontend
8309 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![middle_point_id])
8310 .await
8311 .unwrap();
8312 assert_eq!(
8315 src_delta.text.as_str(),
8316 "\
8317sketch(on = XY) {
8318 point(at = [var 1, var 2])
8319 // describe the middle point
8320 point(at = [var 5, var 6])
8321}
8322"
8323 );
8324
8325 ctx.close().await;
8326 mock_ctx.close().await;
8327 }
8328
8329 #[tokio::test(flavor = "multi_thread")]
8330 async fn test_delete_last_segment_preserves_pre_comment() {
8331 let initial_source = "\
8332sketch(on = XY) {
8333 point(at = [var 1, var 2])
8334 // describe the trailing point
8335 point(at = [var 3, var 4])
8336}
8337";
8338
8339 let program = Program::parse(initial_source).unwrap().0.unwrap();
8340 let mut frontend = FrontendState::new();
8341
8342 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8343 let mock_ctx = ExecutorContext::new_mock(None).await;
8344 let version = Version(0);
8345
8346 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8347 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8348 let sketch_id = sketch_object.id;
8349 let sketch = expect_sketch(sketch_object);
8350
8351 let last_point_id = *sketch.segments.last().unwrap();
8352
8353 let (src_delta, _scene_delta) = frontend
8354 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![last_point_id])
8355 .await
8356 .unwrap();
8357 assert_eq!(
8360 src_delta.text.as_str(),
8361 "\
8362sketch(on = XY) {
8363 point(at = [var 1, var 2])
8364 // describe the trailing point
8365}
8366"
8367 );
8368
8369 ctx.close().await;
8370 mock_ctx.close().await;
8371 }
8372
8373 #[tokio::test(flavor = "multi_thread")]
8374 async fn test_delete_segment_drops_inline_trailing_comment() {
8375 let initial_source = "\
8376sketch(on = XY) {
8377 point(at = [var 1, var 2])
8378 point(at = [var 3, var 4]) // same-line note that gets dropped
8379 point(at = [var 5, var 6])
8380}
8381";
8382
8383 let program = Program::parse(initial_source).unwrap().0.unwrap();
8384 let mut frontend = FrontendState::new();
8385
8386 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8387 let mock_ctx = ExecutorContext::new_mock(None).await;
8388 let version = Version(0);
8389
8390 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8391 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8392 let sketch_id = sketch_object.id;
8393 let sketch = expect_sketch(sketch_object);
8394
8395 let middle_point_id = *sketch.segments.get(1).unwrap();
8396
8397 let (src_delta, _scene_delta) = frontend
8398 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![middle_point_id])
8399 .await
8400 .unwrap();
8401 assert!(
8403 !src_delta.text.contains("same-line note"),
8404 "inline comment should have been removed: {}",
8405 src_delta.text
8406 );
8407
8408 ctx.close().await;
8409 mock_ctx.close().await;
8410 }
8411
8412 #[tokio::test(flavor = "multi_thread")]
8413 async fn test_delete_segments_preserves_block_comments_across_positions() {
8414 let initial_source = "\
8422sketch(on = XY) {
8423 /* above first - moves to middle */
8424 point(at = [var 1, var 2]) /* same-line on first - dropped */
8425 /* above middle - stays */
8426 point(at = [var 3, var 4])
8427 /* above last - moves to trailing meta */
8428 point(at = [var 5, var 6])
8429}
8430";
8431
8432 let program = Program::parse(initial_source).unwrap().0.unwrap();
8433 let mut frontend = FrontendState::new();
8434
8435 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8436 let mock_ctx = ExecutorContext::new_mock(None).await;
8437 let version = Version(0);
8438
8439 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8440 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8441 let sketch_id = sketch_object.id;
8442 let sketch = expect_sketch(sketch_object);
8443
8444 let first_point_id = *sketch.segments.first().unwrap();
8445 let last_point_id = *sketch.segments.last().unwrap();
8446
8447 let (src_delta, _scene_delta) = frontend
8448 .delete_objects(
8449 &mock_ctx,
8450 version,
8451 sketch_id,
8452 Vec::new(),
8453 vec![first_point_id, last_point_id],
8454 )
8455 .await
8456 .unwrap();
8457 assert_eq!(
8458 src_delta.text.as_str(),
8459 "\
8460sketch(on = XY) {
8461 /* above first - moves to middle */
8462 /* above middle - stays */
8463 point(at = [var 3, var 4])
8464 /* above last - moves to trailing meta */
8465}
8466"
8467 );
8468
8469 ctx.close().await;
8470 mock_ctx.close().await;
8471 }
8472
8473 #[tokio::test(flavor = "multi_thread")]
8474 async fn test_edit_line_when_editing_its_start_point() {
8475 let initial_source = "\
8476sketch(on = XY) {
8477 line(start = [var 1, var 2], end = [var 3, var 4])
8478}
8479";
8480
8481 let program = Program::parse(initial_source).unwrap().0.unwrap();
8482
8483 let mut frontend = FrontendState::new();
8484
8485 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8486 let mock_ctx = ExecutorContext::new_mock(None).await;
8487 let version = Version(0);
8488
8489 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8490 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8491 let sketch_id = sketch_object.id;
8492 let sketch = expect_sketch(sketch_object);
8493
8494 let point_id = *sketch.segments.first().unwrap();
8495
8496 let point_ctor = PointCtor {
8497 position: Point2d {
8498 x: Expr::Var(Number {
8499 value: 5.0,
8500 units: NumericSuffix::Inch,
8501 }),
8502 y: Expr::Var(Number {
8503 value: 6.0,
8504 units: NumericSuffix::Inch,
8505 }),
8506 },
8507 };
8508 let segments = vec![ExistingSegmentCtor {
8509 id: point_id,
8510 ctor: SegmentCtor::Point(point_ctor),
8511 }];
8512 let (src_delta, scene_delta) = frontend
8513 .edit_segments(&mock_ctx, version, sketch_id, segments)
8514 .await
8515 .unwrap();
8516 assert_eq!(
8517 src_delta.text.as_str(),
8518 "\
8519sketch(on = XY) {
8520 line(start = [var 5in, var 6in], end = [var 3, var 4])
8521}
8522"
8523 );
8524 assert_eq!(scene_delta.new_objects, vec![]);
8525 assert_eq!(scene_delta.new_graph.objects.len(), 5);
8526
8527 ctx.close().await;
8528 mock_ctx.close().await;
8529 }
8530
8531 #[tokio::test(flavor = "multi_thread")]
8532 async fn test_edit_line_when_editing_its_end_point() {
8533 let initial_source = "\
8534sketch(on = XY) {
8535 line(start = [var 1, var 2], end = [var 3, var 4])
8536}
8537";
8538
8539 let program = Program::parse(initial_source).unwrap().0.unwrap();
8540
8541 let mut frontend = FrontendState::new();
8542
8543 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8544 let mock_ctx = ExecutorContext::new_mock(None).await;
8545 let version = Version(0);
8546
8547 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8548 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8549 let sketch_id = sketch_object.id;
8550 let sketch = expect_sketch(sketch_object);
8551 let point_id = *sketch.segments.get(1).unwrap();
8552
8553 let point_ctor = PointCtor {
8554 position: Point2d {
8555 x: Expr::Var(Number {
8556 value: 5.0,
8557 units: NumericSuffix::Inch,
8558 }),
8559 y: Expr::Var(Number {
8560 value: 6.0,
8561 units: NumericSuffix::Inch,
8562 }),
8563 },
8564 };
8565 let segments = vec![ExistingSegmentCtor {
8566 id: point_id,
8567 ctor: SegmentCtor::Point(point_ctor),
8568 }];
8569 let (src_delta, scene_delta) = frontend
8570 .edit_segments(&mock_ctx, version, sketch_id, segments)
8571 .await
8572 .unwrap();
8573 assert_eq!(
8574 src_delta.text.as_str(),
8575 "\
8576sketch(on = XY) {
8577 line(start = [var 1, var 2], end = [var 5in, var 6in])
8578}
8579"
8580 );
8581 assert_eq!(scene_delta.new_objects, vec![]);
8582 assert_eq!(
8583 scene_delta.new_graph.objects.len(),
8584 5,
8585 "{:#?}",
8586 scene_delta.new_graph.objects
8587 );
8588
8589 ctx.close().await;
8590 mock_ctx.close().await;
8591 }
8592
8593 #[tokio::test(flavor = "multi_thread")]
8594 async fn test_edit_line_with_coincident_feedback() {
8595 let initial_source = "\
8596sketch(on = XY) {
8597 line1 = line(start = [var 1, var 2], end = [var 1, var 2])
8598 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8599 fixed([line1.start, [0, 0]])
8600 coincident([line1.end, line2.start])
8601 equalLength([line1, line2])
8602}
8603";
8604
8605 let program = Program::parse(initial_source).unwrap().0.unwrap();
8606
8607 let mut frontend = FrontendState::new();
8608
8609 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8610 let mock_ctx = ExecutorContext::new_mock(None).await;
8611 let version = Version(0);
8612
8613 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8614 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8615 let sketch_id = sketch_object.id;
8616 let sketch = expect_sketch(sketch_object);
8617 let line2_end_id = *sketch.segments.get(4).unwrap();
8618
8619 let segments = vec![ExistingSegmentCtor {
8620 id: line2_end_id,
8621 ctor: SegmentCtor::Point(PointCtor {
8622 position: Point2d {
8623 x: Expr::Var(Number {
8624 value: 9.0,
8625 units: NumericSuffix::None,
8626 }),
8627 y: Expr::Var(Number {
8628 value: 10.0,
8629 units: NumericSuffix::None,
8630 }),
8631 },
8632 }),
8633 }];
8634 let (src_delta, scene_delta) = frontend
8635 .edit_segments(&mock_ctx, version, sketch_id, segments)
8636 .await
8637 .unwrap();
8638 assert_eq!(
8639 src_delta.text.as_str(),
8640 "\
8641sketch(on = XY) {
8642 line1 = line(start = [var 0, var 0], end = [var 4.14, var 5.32])
8643 line2 = line(start = [var 4.14, var 5.32], end = [var 9, var 10])
8644 fixed([line1.start, [0, 0]])
8645 coincident([line1.end, line2.start])
8646 equalLength([line1, line2])
8647}
8648"
8649 );
8650 assert_eq!(
8651 scene_delta.new_graph.objects.len(),
8652 11,
8653 "{:#?}",
8654 scene_delta.new_graph.objects
8655 );
8656
8657 ctx.close().await;
8658 mock_ctx.close().await;
8659 }
8660
8661 #[tokio::test(flavor = "multi_thread")]
8662 async fn test_edit_segments_persists_solver_feedback_for_next_mock_execute() {
8663 let initial_source = "\
8664sketch(on = XY) {
8665 line1 = line(start = [var 1, var 2], end = [var 1, var 2])
8666 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8667 fixed([line1.start, [0, 0]])
8668 coincident([line1.end, line2.start])
8669 equalLength([line1, line2])
8670}
8671";
8672
8673 let program = Program::parse(initial_source).unwrap().0.unwrap();
8674 let mut frontend = FrontendState::new();
8675 let mock_ctx = ExecutorContext::new_mock(None).await;
8676 let version = Version(0);
8677
8678 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8679 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8680 let sketch_id = sketch_object.id;
8681 let sketch = expect_sketch(sketch_object);
8682 let line2_end_id = *sketch.segments.get(4).unwrap();
8683
8684 let segments = vec![ExistingSegmentCtor {
8685 id: line2_end_id,
8686 ctor: SegmentCtor::Point(PointCtor {
8687 position: Point2d {
8688 x: Expr::Var(Number {
8689 value: 9.0,
8690 units: NumericSuffix::None,
8691 }),
8692 y: Expr::Var(Number {
8693 value: 10.0,
8694 units: NumericSuffix::None,
8695 }),
8696 },
8697 }),
8698 }];
8699 let (edited_source, _) = frontend
8700 .edit_segments(&mock_ctx, version, sketch_id, segments)
8701 .await
8702 .unwrap();
8703
8704 let (mock_source, _) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
8705 assert_eq!(mock_source.text, edited_source.text);
8706
8707 mock_ctx.close().await;
8708 }
8709
8710 #[tokio::test(flavor = "multi_thread")]
8713 async fn test_preview_edit_segments_does_not_persist_solver_feedback() {
8714 let initial_source = "\
8715sketch(on = XY) {
8716 line1 = line(start = [var 1, var 2], end = [var 1, var 2])
8717 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8718 fixed([line1.start, [0, 0]])
8719 coincident([line1.end, line2.start])
8720 equalLength([line1, line2])
8721}
8722";
8723
8724 let program = Program::parse(initial_source).unwrap().0.unwrap();
8725 let mut frontend = FrontendState::new();
8726 let mock_ctx = ExecutorContext::new_mock(None).await;
8727 let version = Version(0);
8728
8729 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8730 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8731 let sketch_id = sketch_object.id;
8732 let sketch = expect_sketch(sketch_object);
8733 let line2_end_id = *sketch.segments.get(4).unwrap();
8734
8735 let segments = vec![ExistingSegmentCtor {
8736 id: line2_end_id,
8737 ctor: SegmentCtor::Point(PointCtor {
8738 position: Point2d {
8739 x: Expr::Var(Number {
8740 value: 9.0,
8741 units: NumericSuffix::None,
8742 }),
8743 y: Expr::Var(Number {
8744 value: 10.0,
8745 units: NumericSuffix::None,
8746 }),
8747 },
8748 }),
8749 }];
8750 let (preview_source, preview_delta) = frontend
8751 .edit_segments_with_options(
8752 &mock_ctx,
8753 version,
8754 sketch_id,
8755 segments,
8756 EditSegmentsOptions {
8757 anchor_segment_ids: Some(vec![line2_end_id]),
8758 drag_anchors: Vec::new(),
8759 commit_solved_initial_guesses: false,
8760 },
8761 )
8762 .await
8763 .unwrap();
8764
8765 assert!(
8766 !preview_delta.exec_outcome.var_solutions.is_empty(),
8767 "preview solve should still solve and return geometry feedback"
8768 );
8769 assert!(
8770 preview_source
8771 .text
8772 .contains("line1 = line(start = [var 1, var 2], end = [var 1, var 2])")
8773 );
8774 assert!(
8775 preview_source
8776 .text
8777 .contains("line2 = line(start = [var 5, var 6], end = [var 9, var 10])")
8778 );
8779
8780 let (mock_source, _) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
8781 assert_eq!(mock_source.text, preview_source.text);
8782
8783 mock_ctx.close().await;
8784 }
8785
8786 #[tokio::test(flavor = "multi_thread")]
8787 async fn test_add_constraint_persists_solver_feedback_for_next_mock_execute() {
8788 let initial_source = "\
8789sketch(on = XY) {
8790 line1 = line(start = [var 0, var 0], end = [var 10, var 0])
8791}
8792";
8793
8794 let program = Program::parse(initial_source).unwrap().0.unwrap();
8795 let mut frontend = FrontendState::new();
8796 let mock_ctx = ExecutorContext::new_mock(None).await;
8797 let version = Version(0);
8798
8799 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8800 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8801 let sketch_id = sketch_object.id;
8802 let sketch = expect_sketch(sketch_object);
8803 let line_end_id = *sketch.segments.get(1).unwrap();
8804
8805 let constraint = Constraint::Fixed(Fixed {
8806 points: vec![FixedPoint {
8807 point: line_end_id,
8808 position: Point2d {
8809 x: Number {
8810 value: 20.0,
8811 units: NumericSuffix::Mm,
8812 },
8813 y: Number {
8814 value: 0.0,
8815 units: NumericSuffix::Mm,
8816 },
8817 },
8818 }],
8819 });
8820 let (constraint_source, _) = frontend
8821 .add_constraint(&mock_ctx, version, sketch_id, constraint)
8822 .await
8823 .unwrap();
8824
8825 assert!(
8826 constraint_source
8827 .text
8828 .contains("line1 = line(start = [var 0, var 0], end = [var 20, var 0])"),
8829 "{}",
8830 constraint_source.text
8831 );
8832 let (mock_source, _) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
8833 assert_eq!(mock_source.text, constraint_source.text);
8834
8835 mock_ctx.close().await;
8836 }
8837
8838 #[test]
8839 fn test_no_solver_feedback_preserves_original_source() {
8840 let initial_source = "\
8841@settings(defaultLengthUnit = in, kclVersion = 2.0)
8842cylinder = startSketchOn(XY)
8843 |> circle(center= [0, 0], radius= 22)
8844 |> extrude(length = 14)
8845";
8846 let mut frontend = FrontendState::new();
8847 frontend.program = Program::parse(initial_source).unwrap().0.unwrap();
8848 let outcome = ExecOutcome {
8849 variables: Default::default(),
8850 operations: Default::default(),
8851 artifact_graph: Default::default(),
8852 scene_objects: Default::default(),
8853 source_range_to_object: Default::default(),
8854 var_solutions: Default::default(),
8855 issues: Default::default(),
8856 filenames: Default::default(),
8857 default_planes: Default::default(),
8858 };
8859
8860 let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
8861
8862 assert_eq!(source_delta.text, initial_source);
8863 }
8864
8865 #[tokio::test(flavor = "multi_thread")]
8868 async fn test_edit_segments_with_anchor_ids_limits_drag_fixed_constraints() {
8869 let initial_source = "\
8870sketch(on = XY) {
8871 point1 = point(at = [var 0mm, var 0mm])
8872 point2 = point(at = [var 0mm, var 0mm])
8873 coincident([point1, point2])
8874}
8875";
8876
8877 let program = Program::parse(initial_source).unwrap().0.unwrap();
8878 let mut frontend = FrontendState::new();
8879 let mock_ctx = ExecutorContext::new_mock(None).await;
8880 let version = Version(0);
8881
8882 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8883 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8884 let sketch_id = sketch_object.id;
8885 let sketch = expect_sketch(sketch_object);
8886 let point1_id = sketch.segments[0];
8887 let point2_id = sketch.segments[1];
8888
8889 let segments = vec![
8890 ExistingSegmentCtor {
8891 id: point1_id,
8892 ctor: SegmentCtor::Point(PointCtor {
8893 position: point_expr_mm(10.0, 0.0),
8894 }),
8895 },
8896 ExistingSegmentCtor {
8897 id: point2_id,
8898 ctor: SegmentCtor::Point(PointCtor {
8899 position: point_expr_mm(100.0, 0.0),
8900 }),
8901 },
8902 ];
8903 let (_, scene_delta) = frontend
8904 .edit_segments_with_options(
8905 &mock_ctx,
8906 version,
8907 sketch_id,
8908 segments,
8909 EditSegmentsOptions {
8910 anchor_segment_ids: Some(vec![point1_id]),
8911 drag_anchors: Vec::new(),
8912 commit_solved_initial_guesses: true,
8913 },
8914 )
8915 .await
8916 .unwrap();
8917
8918 assert_point_position_close(
8919 point_position(&scene_delta.new_graph, point1_id),
8920 point_number_mm(10.0, 0.0),
8921 );
8922 assert_point_position_close(
8923 point_position(&scene_delta.new_graph, point2_id),
8924 point_number_mm(10.0, 0.0),
8925 );
8926
8927 mock_ctx.close().await;
8928 }
8929
8930 fn collect_sketch_var_literals_with_value(program: &Program, value: f64) -> Vec<(SourceRange, ast::NodePath)> {
8933 use std::cell::RefCell;
8934 struct Collector {
8935 target: f64,
8936 out: RefCell<Vec<(SourceRange, ast::NodePath)>>,
8937 }
8938 impl<'a> crate::walk::Visitor<'a> for &Collector {
8939 type Error = crate::front::Error;
8940 fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
8941 if let crate::walk::Node::SketchVar(sketch_var) = node
8942 && let (Some(initial), Some(node_path)) = (&sketch_var.initial, &sketch_var.node_path)
8943 && (initial.value - self.target).abs() < 1e-9
8944 {
8945 self.out
8946 .borrow_mut()
8947 .push((SourceRange::from(initial.as_ref()), node_path.clone()));
8948 }
8949 for child in node.children().iter() {
8950 if !child.visit(*self)? {
8951 return Ok(false);
8952 }
8953 }
8954 Ok(true)
8955 }
8956 }
8957 let collector = Collector {
8958 target: value,
8959 out: Default::default(),
8960 };
8961 let _ = crate::walk::Node::from(&program.ast).visit(&collector);
8962 collector.out.into_inner()
8963 }
8964
8965 fn collect_all_sketch_vars(program: &Program) -> Vec<(SourceRange, ast::NodePath)> {
8968 use std::cell::RefCell;
8969 struct Collector {
8970 out: RefCell<Vec<(SourceRange, ast::NodePath)>>,
8971 }
8972 impl<'a> crate::walk::Visitor<'a> for &Collector {
8973 type Error = crate::front::Error;
8974 fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
8975 if let crate::walk::Node::SketchVar(sketch_var) = node
8976 && let Some(node_path) = &sketch_var.node_path
8977 {
8978 self.out
8979 .borrow_mut()
8980 .push((SourceRange::from(sketch_var), node_path.clone()));
8981 }
8982 for child in node.children().iter() {
8983 if !child.visit(*self)? {
8984 return Ok(false);
8985 }
8986 }
8987 Ok(true)
8988 }
8989 }
8990 let collector = Collector {
8991 out: Default::default(),
8992 };
8993 let _ = crate::walk::Node::from(&program.ast).visit(&collector);
8994 collector.out.into_inner()
8995 }
8996
8997 fn empty_exec_outcome_with_var_solutions(
8998 var_solutions: Vec<(SourceRange, Option<ast::NodePath>, Number)>,
8999 ) -> ExecOutcome {
9000 ExecOutcome {
9001 variables: Default::default(),
9002 operations: Default::default(),
9003 artifact_graph: Default::default(),
9004 scene_objects: Default::default(),
9005 source_range_to_object: Default::default(),
9006 var_solutions,
9007 issues: Default::default(),
9008 filenames: Default::default(),
9009 default_planes: Default::default(),
9010 }
9011 }
9012
9013 #[test]
9017 fn test_commit_var_solution_by_node_path_updates_sketch_var() {
9018 let initial_source = "\
9019sketch(on = XY) {
9020 line1 = line(start = [var 0, var 0], end = [var 10mm, var 0])
9021}
9022";
9023 let program = Program::parse(initial_source).unwrap().0.unwrap();
9024 let matches = collect_sketch_var_literals_with_value(&program, 10.0);
9025 assert_eq!(matches.len(), 1, "expected exactly one `var 10mm`");
9026 let (literal_range, node_path) = matches.into_iter().next().unwrap();
9027
9028 let mut frontend = FrontendState::new();
9029 frontend.program = program;
9030
9031 let outcome = empty_exec_outcome_with_var_solutions(vec![(
9032 literal_range,
9033 Some(node_path),
9034 Number {
9035 value: 25.0,
9036 units: NumericSuffix::Mm,
9037 },
9038 )]);
9039
9040 let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9041
9042 assert_eq!(
9043 source_delta.text,
9044 "\
9045sketch(on = XY) {
9046 line1 = line(start = [var 0, var 0], end = [var 25mm, var 0])
9047}
9048",
9049 );
9050 }
9051
9052 #[test]
9059 fn test_commit_var_solution_survives_whitespace_shift_earlier_in_file() {
9060 let compact_source = "\
9061sketch(on = XY) {
9062 line1 = line(start = [var 0, var 0], end = [var 10mm, var 0])
9063}
9064";
9065 let padded_source = "\
9066// added comment\n// added comment\n\nsketch(on = XY) {
9067 line1 = line(start = [var 0, var 0], end = [var 10mm, var 0])
9068}
9069";
9070 let compact_program = Program::parse(compact_source).unwrap().0.unwrap();
9071 let padded_program = Program::parse(padded_source).unwrap().0.unwrap();
9072
9073 let compact_match = collect_sketch_var_literals_with_value(&compact_program, 10.0)
9074 .into_iter()
9075 .next()
9076 .expect("expected `var 10mm` in compact source");
9077 let padded_match = collect_sketch_var_literals_with_value(&padded_program, 10.0)
9078 .into_iter()
9079 .next()
9080 .expect("expected `var 10mm` in padded source");
9081
9082 assert_ne!(
9083 compact_match.0, padded_match.0,
9084 "byte offsets must differ for this test to be meaningful"
9085 );
9086 assert_eq!(
9087 compact_match.1, padded_match.1,
9088 "node paths must agree across whitespace; that's the whole point of NodePath",
9089 );
9090
9091 let mut frontend = FrontendState::new();
9092 frontend.program = padded_program;
9093
9094 let outcome = empty_exec_outcome_with_var_solutions(vec![(
9096 compact_match.0,
9097 Some(compact_match.1),
9098 Number {
9099 value: 30.0,
9100 units: NumericSuffix::Mm,
9101 },
9102 )]);
9103
9104 let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9105
9106 assert_eq!(
9107 source_delta.text,
9108 "\
9109// added comment
9110// added comment
9111
9112sketch(on = XY) {
9113 line1 = line(start = [var 0, var 0], end = [var 30mm, var 0])
9114}
9115",
9116 );
9117 }
9118
9119 #[test]
9123 fn test_commit_var_solution_node_path_wins_when_source_range_points_at_wrong_var() {
9124 let initial_source = "\
9125sketch(on = XY) {
9126 line1 = line(start = [var 10mm, var 0mm], end = [var 20mm, var 0mm])
9127}
9128";
9129 let program = Program::parse(initial_source).unwrap().0.unwrap();
9130
9131 let var_10 = collect_sketch_var_literals_with_value(&program, 10.0)
9132 .into_iter()
9133 .next()
9134 .expect("expected `var 10mm`");
9135 let var_20 = collect_sketch_var_literals_with_value(&program, 20.0)
9136 .into_iter()
9137 .next()
9138 .expect("expected `var 20mm`");
9139
9140 let mut frontend = FrontendState::new();
9141 frontend.program = program;
9142
9143 let outcome = empty_exec_outcome_with_var_solutions(vec![(
9145 var_20.0,
9146 Some(var_10.1),
9147 Number {
9148 value: 33.0,
9149 units: NumericSuffix::Mm,
9150 },
9151 )]);
9152
9153 let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9154
9155 assert_eq!(
9156 source_delta.text,
9157 "\
9158sketch(on = XY) {
9159 line1 = line(start = [var 33mm, var 0mm], end = [var 20mm, var 0mm])
9160}
9161",
9162 );
9163 }
9164
9165 #[test]
9172 fn test_commit_var_solution_writes_back_into_bare_var() {
9173 let initial_source = "\
9174@settings(experimentalFeatures = allow, kclVersion = 2.0)
9175sketch(on = XY) {
9176 line1 = line(start = [var, var 0mm], end = [var 10mm, var 0])
9177}
9178";
9179 let program = Program::parse(initial_source).unwrap().0.unwrap();
9180
9181 let bare = collect_all_sketch_vars(&program)
9184 .into_iter()
9185 .find(|(range, _)| {
9186 range.end() - range.start() == 3
9188 })
9189 .expect("expected at least one bare `var`");
9190
9191 let mut frontend = FrontendState::new();
9192 frontend.program = program;
9193
9194 let outcome = empty_exec_outcome_with_var_solutions(vec![(
9195 bare.0,
9196 Some(bare.1),
9197 Number {
9198 value: 7.0,
9199 units: NumericSuffix::Mm,
9200 },
9201 )]);
9202
9203 let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9204
9205 assert_eq!(
9209 source_delta.text,
9210 "\
9211@settings(experimentalFeatures = allow, kclVersion = 2.0)
9212
9213sketch(on = XY) {
9214 line1 = line(start = [var 7mm, var 0mm], end = [var 10mm, var 0])
9215}
9216",
9217 );
9218 }
9219
9220 #[tokio::test(flavor = "multi_thread")]
9221 async fn test_delete_point_without_var() {
9222 let initial_source = "\
9223sketch(on = XY) {
9224 point(at = [var 1, var 2])
9225 point(at = [var 3, var 4])
9226 point(at = [var 5, var 6])
9227}
9228";
9229
9230 let program = Program::parse(initial_source).unwrap().0.unwrap();
9231
9232 let mut frontend = FrontendState::new();
9233
9234 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9235 let mock_ctx = ExecutorContext::new_mock(None).await;
9236 let version = Version(0);
9237
9238 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9239 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9240 let sketch_id = sketch_object.id;
9241 let sketch = expect_sketch(sketch_object);
9242
9243 let point_id = *sketch.segments.get(1).unwrap();
9244
9245 let (src_delta, scene_delta) = frontend
9246 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
9247 .await
9248 .unwrap();
9249 assert_eq!(
9250 src_delta.text.as_str(),
9251 "\
9252sketch(on = XY) {
9253 point(at = [var 1, var 2])
9254 point(at = [var 5, var 6])
9255}
9256"
9257 );
9258 assert_eq!(scene_delta.new_objects, vec![]);
9259 assert_eq!(scene_delta.new_graph.objects.len(), 4);
9260
9261 ctx.close().await;
9262 mock_ctx.close().await;
9263 }
9264
9265 #[tokio::test(flavor = "multi_thread")]
9266 async fn test_delete_point_with_var() {
9267 let initial_source = "\
9268sketch(on = XY) {
9269 point(at = [var 1, var 2])
9270 point1 = point(at = [var 3, var 4])
9271 point(at = [var 5, var 6])
9272}
9273";
9274
9275 let program = Program::parse(initial_source).unwrap().0.unwrap();
9276
9277 let mut frontend = FrontendState::new();
9278
9279 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9280 let mock_ctx = ExecutorContext::new_mock(None).await;
9281 let version = Version(0);
9282
9283 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9284 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9285 let sketch_id = sketch_object.id;
9286 let sketch = expect_sketch(sketch_object);
9287
9288 let point_id = *sketch.segments.get(1).unwrap();
9289
9290 let (src_delta, scene_delta) = frontend
9291 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
9292 .await
9293 .unwrap();
9294 assert_eq!(
9295 src_delta.text.as_str(),
9296 "\
9297sketch(on = XY) {
9298 point(at = [var 1, var 2])
9299 point(at = [var 5, var 6])
9300}
9301"
9302 );
9303 assert_eq!(scene_delta.new_objects, vec![]);
9304 assert_eq!(scene_delta.new_graph.objects.len(), 4);
9305
9306 ctx.close().await;
9307 mock_ctx.close().await;
9308 }
9309
9310 #[tokio::test(flavor = "multi_thread")]
9311 async fn test_delete_multiple_points() {
9312 let initial_source = "\
9313sketch(on = XY) {
9314 point(at = [var 1, var 2])
9315 point1 = point(at = [var 3, var 4])
9316 point(at = [var 5, var 6])
9317}
9318";
9319
9320 let program = Program::parse(initial_source).unwrap().0.unwrap();
9321
9322 let mut frontend = FrontendState::new();
9323
9324 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9325 let mock_ctx = ExecutorContext::new_mock(None).await;
9326 let version = Version(0);
9327
9328 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9329 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9330 let sketch_id = sketch_object.id;
9331
9332 let sketch = expect_sketch(sketch_object);
9333
9334 let point1_id = *sketch.segments.first().unwrap();
9335 let point2_id = *sketch.segments.get(1).unwrap();
9336
9337 let (src_delta, scene_delta) = frontend
9338 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point1_id, point2_id])
9339 .await
9340 .unwrap();
9341 assert_eq!(
9342 src_delta.text.as_str(),
9343 "\
9344sketch(on = XY) {
9345 point(at = [var 5, var 6])
9346}
9347"
9348 );
9349 assert_eq!(scene_delta.new_objects, vec![]);
9350 assert_eq!(scene_delta.new_graph.objects.len(), 3);
9351
9352 ctx.close().await;
9353 mock_ctx.close().await;
9354 }
9355
9356 #[tokio::test(flavor = "multi_thread")]
9357 async fn test_delete_coincident_constraint() {
9358 let initial_source = "\
9359sketch(on = XY) {
9360 point1 = point(at = [var 1, var 2])
9361 point2 = point(at = [var 3, var 4])
9362 coincident([point1, point2])
9363 point(at = [var 5, var 6])
9364}
9365";
9366
9367 let program = Program::parse(initial_source).unwrap().0.unwrap();
9368
9369 let mut frontend = FrontendState::new();
9370
9371 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9372 let mock_ctx = ExecutorContext::new_mock(None).await;
9373 let version = Version(0);
9374
9375 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9376 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9377 let sketch_id = sketch_object.id;
9378 let sketch = expect_sketch(sketch_object);
9379
9380 let coincident_id = *sketch.constraints.first().unwrap();
9381
9382 let (src_delta, scene_delta) = frontend
9383 .delete_objects(&mock_ctx, version, sketch_id, vec![coincident_id], Vec::new())
9384 .await
9385 .unwrap();
9386 assert_eq!(
9387 src_delta.text.as_str(),
9388 "\
9389sketch(on = XY) {
9390 point1 = point(at = [var 1, var 2])
9391 point2 = point(at = [var 3, var 4])
9392 point(at = [var 5, var 6])
9393}
9394"
9395 );
9396 assert_eq!(scene_delta.new_objects, vec![]);
9397 assert_eq!(scene_delta.new_graph.objects.len(), 5);
9398
9399 ctx.close().await;
9400 mock_ctx.close().await;
9401 }
9402
9403 #[tokio::test(flavor = "multi_thread")]
9404 async fn test_delete_line_cascades_to_coincident_constraint() {
9405 let initial_source = "\
9406sketch(on = XY) {
9407 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9408 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9409 coincident([line1.end, line2.start])
9410}
9411";
9412
9413 let program = Program::parse(initial_source).unwrap().0.unwrap();
9414
9415 let mut frontend = FrontendState::new();
9416
9417 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9418 let mock_ctx = ExecutorContext::new_mock(None).await;
9419 let version = Version(0);
9420
9421 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9422 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9423 let sketch_id = sketch_object.id;
9424 let sketch = expect_sketch(sketch_object);
9425 let line_id = *sketch.segments.get(5).unwrap();
9426
9427 let (src_delta, scene_delta) = frontend
9428 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
9429 .await
9430 .unwrap();
9431 assert_eq!(
9432 src_delta.text.as_str(),
9433 "\
9434sketch(on = XY) {
9435 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9436}
9437"
9438 );
9439 assert_eq!(
9440 scene_delta.new_graph.objects.len(),
9441 5,
9442 "{:#?}",
9443 scene_delta.new_graph.objects
9444 );
9445
9446 ctx.close().await;
9447 mock_ctx.close().await;
9448 }
9449
9450 #[tokio::test(flavor = "multi_thread")]
9451 async fn test_delete_line_cascades_to_distance_constraint() {
9452 let initial_source = "\
9453sketch(on = XY) {
9454 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9455 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9456 distance([line1.end, line2.start]) == 10mm
9457}
9458";
9459
9460 let program = Program::parse(initial_source).unwrap().0.unwrap();
9461
9462 let mut frontend = FrontendState::new();
9463
9464 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9465 let mock_ctx = ExecutorContext::new_mock(None).await;
9466 let version = Version(0);
9467
9468 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9469 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9470 let sketch_id = sketch_object.id;
9471 let sketch = expect_sketch(sketch_object);
9472 let line_id = *sketch.segments.get(5).unwrap();
9473
9474 let (src_delta, scene_delta) = frontend
9475 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
9476 .await
9477 .unwrap();
9478 assert_eq!(
9479 src_delta.text.as_str(),
9480 "\
9481sketch(on = XY) {
9482 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9483}
9484"
9485 );
9486 assert_eq!(
9487 scene_delta.new_graph.objects.len(),
9488 5,
9489 "{:#?}",
9490 scene_delta.new_graph.objects
9491 );
9492
9493 ctx.close().await;
9494 mock_ctx.close().await;
9495 }
9496
9497 #[tokio::test(flavor = "multi_thread")]
9498 async fn test_delete_point_cascades_to_horizontal_distance_constraint() {
9499 let initial_source = "\
9500sketch(on = XY) {
9501 point1 = point(at = [var 1, var 2])
9502 point2 = point(at = [var 3, var 4])
9503 horizontalDistance([point1, point2]) == 10mm
9504}
9505";
9506
9507 let program = Program::parse(initial_source).unwrap().0.unwrap();
9508
9509 let mut frontend = FrontendState::new();
9510
9511 let mock_ctx = ExecutorContext::new_mock(None).await;
9512 let version = Version(0);
9513
9514 frontend.program = program.clone();
9515 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9516 frontend.update_state_after_exec(outcome, true);
9517 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9518 let sketch_id = sketch_object.id;
9519 let sketch = expect_sketch(sketch_object);
9520 let point2_id = *sketch.segments.get(1).unwrap();
9521
9522 let (src_delta, scene_delta) = frontend
9523 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point2_id])
9524 .await
9525 .unwrap();
9526 assert_eq!(
9527 src_delta.text.as_str(),
9528 "\
9529sketch(on = XY) {
9530 point1 = point(at = [var 1, var 2])
9531}
9532"
9533 );
9534 assert_eq!(
9535 scene_delta.new_graph.objects.len(),
9536 3,
9537 "{:#?}",
9538 scene_delta.new_graph.objects
9539 );
9540
9541 mock_ctx.close().await;
9542 }
9543
9544 #[tokio::test(flavor = "multi_thread")]
9545 async fn test_delete_line_cascades_to_fixed_constraint() {
9546 let initial_source = "\
9547sketch(on = XY) {
9548 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9549 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9550 fixed([line1.start, [0, 0]])
9551}
9552";
9553
9554 let program = Program::parse(initial_source).unwrap().0.unwrap();
9555
9556 let mut frontend = FrontendState::new();
9557
9558 let mock_ctx = ExecutorContext::new_mock(None).await;
9559 let version = Version(0);
9560
9561 frontend.program = program.clone();
9562 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9563 frontend.update_state_after_exec(outcome, true);
9564 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9565 let sketch_id = sketch_object.id;
9566 let sketch = expect_sketch(sketch_object);
9567 let line1_id = *sketch.segments.get(2).unwrap();
9568
9569 let (src_delta, scene_delta) = frontend
9570 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
9571 .await
9572 .unwrap();
9573 assert_eq!(
9574 src_delta.text.as_str(),
9575 "\
9576sketch(on = XY) {
9577 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9578}
9579"
9580 );
9581 assert_eq!(
9582 scene_delta.new_graph.objects.len(),
9583 5,
9584 "{:#?}",
9585 scene_delta.new_graph.objects
9586 );
9587
9588 mock_ctx.close().await;
9589 }
9590
9591 #[tokio::test(flavor = "multi_thread")]
9592 async fn test_delete_line_cascades_to_midpoint_constraint() {
9593 let initial_source = "\
9594sketch(on = XY) {
9595 point1 = point(at = [var 1, var 2])
9596 line1 = line(start = [var 0, var 0], end = [var 6, var 4])
9597 midpoint(line1, point = point1)
9598}
9599";
9600
9601 let program = Program::parse(initial_source).unwrap().0.unwrap();
9602
9603 let mut frontend = FrontendState::new();
9604
9605 let mock_ctx = ExecutorContext::new_mock(None).await;
9606 let version = Version(0);
9607
9608 frontend.program = program.clone();
9609 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9610 frontend.update_state_after_exec(outcome, true);
9611 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9612 let sketch_id = sketch_object.id;
9613 let sketch = expect_sketch(sketch_object);
9614 let line1_id = *sketch.segments.get(3).unwrap();
9615
9616 let (src_delta, scene_delta) = frontend
9617 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
9618 .await
9619 .unwrap();
9620 assert_eq!(
9621 src_delta.text.as_str(),
9622 "\
9623sketch(on = XY) {
9624 point1 = point(at = [var 1, var 2])
9625}
9626"
9627 );
9628 assert_eq!(
9629 scene_delta.new_graph.objects.len(),
9630 3,
9631 "{:#?}",
9632 scene_delta.new_graph.objects
9633 );
9634
9635 mock_ctx.close().await;
9636 }
9637
9638 #[tokio::test(flavor = "multi_thread")]
9639 async fn test_delete_point_preserves_multiline_coincident_constraint() {
9640 let initial_source = "\
9641sketch(on = XY) {
9642 point1 = point(at = [var 1, var 2])
9643 point2 = point(at = [var 3, var 4])
9644 point3 = point(at = [var 5, var 6])
9645 coincident([point1, point2, point3])
9646}
9647";
9648
9649 let program = Program::parse(initial_source).unwrap().0.unwrap();
9650
9651 let mut frontend = FrontendState::new();
9652
9653 let mock_ctx = ExecutorContext::new_mock(None).await;
9654 let version = Version(0);
9655
9656 frontend.program = program.clone();
9657 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9658 frontend.update_state_after_exec(outcome, true);
9659 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9660 let sketch_id = sketch_object.id;
9661 let sketch = expect_sketch(sketch_object);
9662 let point3_id = *sketch.segments.get(2).unwrap();
9663
9664 let (src_delta, scene_delta) = frontend
9665 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point3_id])
9666 .await
9667 .unwrap();
9668 assert!(src_delta.text.contains("point1 = point("), "{}", src_delta.text);
9669 assert!(src_delta.text.contains("point2 = point("), "{}", src_delta.text);
9670 assert!(!src_delta.text.contains("point3 = point("), "{}", src_delta.text);
9671 assert!(
9672 src_delta.text.contains("coincident([point1, point2])"),
9673 "{}",
9674 src_delta.text
9675 );
9676
9677 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9678 let sketch = expect_sketch(sketch_object);
9679 assert_eq!(sketch.segments.len(), 2);
9680 assert_eq!(sketch.constraints.len(), 1);
9681
9682 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
9683 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9684 panic!("Expected constraint object");
9685 };
9686 let Constraint::Coincident(coincident) = constraint else {
9687 panic!("Expected coincident constraint");
9688 };
9689 assert_eq!(
9690 coincident.segments,
9691 sketch
9692 .segments
9693 .iter()
9694 .copied()
9695 .map(Into::into)
9696 .collect::<Vec<ConstraintSegment>>()
9697 );
9698
9699 mock_ctx.close().await;
9700 }
9701
9702 #[tokio::test(flavor = "multi_thread")]
9703 async fn test_delete_line_preserves_multiline_equal_length_constraint() {
9704 let initial_source = "\
9705sketch(on = XY) {
9706 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9707 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9708 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
9709 equalLength([line1, line2, line3])
9710}
9711";
9712
9713 let program = Program::parse(initial_source).unwrap().0.unwrap();
9714
9715 let mut frontend = FrontendState::new();
9716
9717 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9718 let mock_ctx = ExecutorContext::new_mock(None).await;
9719 let version = Version(0);
9720
9721 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9722 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9723 let sketch_id = sketch_object.id;
9724 let sketch = expect_sketch(sketch_object);
9725 let line3_id = *sketch.segments.get(8).unwrap();
9726
9727 let (src_delta, scene_delta) = frontend
9728 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line3_id])
9729 .await
9730 .unwrap();
9731 assert_eq!(
9732 src_delta.text.as_str(),
9733 "\
9734sketch(on = XY) {
9735 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9736 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9737 equalLength([line1, line2])
9738}
9739"
9740 );
9741
9742 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9743 let sketch = expect_sketch(sketch_object);
9744 assert_eq!(sketch.constraints.len(), 1);
9745
9746 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
9747 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9748 panic!("Expected constraint object");
9749 };
9750 let Constraint::LinesEqualLength(lines_equal_length) = constraint else {
9751 panic!("Expected lines equal length constraint");
9752 };
9753 assert_eq!(lines_equal_length.lines.len(), 2);
9754
9755 ctx.close().await;
9756 mock_ctx.close().await;
9757 }
9758
9759 #[tokio::test(flavor = "multi_thread")]
9760 async fn test_delete_line_preserves_multiline_horizontal_constraint() {
9761 let initial_source = "\
9762sketch(on = XY) {
9763 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9764 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9765 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
9766 horizontal([line1.end, line2.start, line3.start])
9767}
9768";
9769
9770 let program = Program::parse(initial_source).unwrap().0.unwrap();
9771
9772 let mut frontend = FrontendState::new();
9773
9774 let mock_ctx = ExecutorContext::new_mock(None).await;
9775 let version = Version(0);
9776
9777 frontend.program = program.clone();
9778 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9779 frontend.update_state_after_exec(outcome, true);
9780 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9781 let sketch_id = sketch_object.id;
9782 let sketch = expect_sketch(sketch_object);
9783 let line1_id = *sketch.segments.get(2).unwrap();
9784
9785 let (src_delta, scene_delta) = frontend
9786 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
9787 .await
9788 .unwrap();
9789 assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
9790 assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
9791 assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
9792 assert!(
9793 src_delta.text.contains("horizontal([line2.start, line3.start])"),
9794 "{}",
9795 src_delta.text
9796 );
9797
9798 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9799 let sketch = expect_sketch(sketch_object);
9800 assert_eq!(sketch.constraints.len(), 1);
9801
9802 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
9803 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9804 panic!("Expected constraint object");
9805 };
9806 let Constraint::Horizontal(Horizontal::Points { points }) = constraint else {
9807 panic!("Expected horizontal points constraint");
9808 };
9809 let remaining_points = vec![sketch.segments[0].into(), sketch.segments[3].into()];
9810 assert_eq!(*points, remaining_points);
9811
9812 mock_ctx.close().await;
9813 }
9814
9815 #[tokio::test(flavor = "multi_thread")]
9816 async fn test_delete_line_preserves_multiline_vertical_constraint() {
9817 let initial_source = "\
9818sketch(on = XY) {
9819 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9820 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9821 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
9822 vertical([line1.end, line2.start, line3.start])
9823}
9824";
9825
9826 let program = Program::parse(initial_source).unwrap().0.unwrap();
9827
9828 let mut frontend = FrontendState::new();
9829
9830 let mock_ctx = ExecutorContext::new_mock(None).await;
9831 let version = Version(0);
9832
9833 frontend.program = program.clone();
9834 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9835 frontend.update_state_after_exec(outcome, true);
9836 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9837 let sketch_id = sketch_object.id;
9838 let sketch = expect_sketch(sketch_object);
9839 let line1_id = *sketch.segments.get(2).unwrap();
9840
9841 let (src_delta, scene_delta) = frontend
9842 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
9843 .await
9844 .unwrap();
9845 assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
9846 assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
9847 assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
9848 assert!(
9849 src_delta.text.contains("vertical([line2.start, line3.start])"),
9850 "{}",
9851 src_delta.text
9852 );
9853
9854 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9855 let sketch = expect_sketch(sketch_object);
9856 assert_eq!(sketch.constraints.len(), 1);
9857
9858 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
9859 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9860 panic!("Expected constraint object");
9861 };
9862 let Constraint::Vertical(Vertical::Points { points }) = constraint else {
9863 panic!("Expected vertical points constraint");
9864 };
9865 let remaining_points = vec![sketch.segments[0].into(), sketch.segments[3].into()];
9866 assert_eq!(*points, remaining_points);
9867
9868 mock_ctx.close().await;
9869 }
9870
9871 #[tokio::test(flavor = "multi_thread")]
9872 async fn test_delete_line_preserves_multiline_coincident_constraint() {
9873 let initial_source = "\
9874sketch(on = XY) {
9875 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9876 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9877 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
9878 coincident([line1.end, line2.start, line3.start])
9879}
9880";
9881
9882 let program = Program::parse(initial_source).unwrap().0.unwrap();
9883
9884 let mut frontend = FrontendState::new();
9885
9886 let mock_ctx = ExecutorContext::new_mock(None).await;
9887 let version = Version(0);
9888
9889 frontend.program = program.clone();
9890 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9891 frontend.update_state_after_exec(outcome, true);
9892 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9893 let sketch_id = sketch_object.id;
9894 let sketch = expect_sketch(sketch_object);
9895 let line1_id = *sketch.segments.get(2).unwrap();
9896
9897 let (src_delta, scene_delta) = frontend
9898 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
9899 .await
9900 .unwrap();
9901 assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
9902 assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
9903 assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
9904 assert!(
9905 src_delta.text.contains("coincident([line2.start, line3.start])"),
9906 "{}",
9907 src_delta.text
9908 );
9909
9910 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9911 let sketch = expect_sketch(sketch_object);
9912 assert_eq!(sketch.constraints.len(), 1);
9913
9914 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
9915 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9916 panic!("Expected constraint object");
9917 };
9918 let Constraint::Coincident(coincident) = constraint else {
9919 panic!("Expected coincident constraint");
9920 };
9921 let remaining_segments = vec![sketch.segments[0].into(), sketch.segments[3].into()];
9922 assert_eq!(coincident.segments, remaining_segments);
9923
9924 mock_ctx.close().await;
9925 }
9926
9927 #[tokio::test(flavor = "multi_thread")]
9928 async fn test_delete_lines_removes_multiline_equal_length_constraint_below_minimum() {
9929 let initial_source = "\
9930sketch(on = XY) {
9931 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9932 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9933 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
9934 equalLength([line1, line2, line3])
9935}
9936";
9937
9938 let program = Program::parse(initial_source).unwrap().0.unwrap();
9939
9940 let mut frontend = FrontendState::new();
9941
9942 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9943 let mock_ctx = ExecutorContext::new_mock(None).await;
9944 let version = Version(0);
9945
9946 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9947 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9948 let sketch_id = sketch_object.id;
9949 let sketch = expect_sketch(sketch_object);
9950 let line2_id = *sketch.segments.get(5).unwrap();
9951 let line3_id = *sketch.segments.get(8).unwrap();
9952
9953 let (src_delta, scene_delta) = frontend
9954 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line2_id, line3_id])
9955 .await
9956 .unwrap();
9957 assert_eq!(
9958 src_delta.text.as_str(),
9959 "\
9960sketch(on = XY) {
9961 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9962}
9963"
9964 );
9965
9966 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9967 let sketch = expect_sketch(sketch_object);
9968 assert!(sketch.constraints.is_empty());
9969
9970 ctx.close().await;
9971 mock_ctx.close().await;
9972 }
9973
9974 #[tokio::test(flavor = "multi_thread")]
9975 async fn test_delete_line_preserves_multiline_parallel_constraint() {
9976 let initial_source = "\
9977sketch(on = XY) {
9978 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9979 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9980 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
9981 parallel([line1, line2, line3])
9982}
9983";
9984
9985 let program = Program::parse(initial_source).unwrap().0.unwrap();
9986
9987 let mut frontend = FrontendState::new();
9988
9989 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9990 let mock_ctx = ExecutorContext::new_mock(None).await;
9991 let version = Version(0);
9992
9993 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9994 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9995 let sketch_id = sketch_object.id;
9996 let sketch = expect_sketch(sketch_object);
9997 let line3_id = *sketch.segments.get(8).unwrap();
9998
9999 let (src_delta, scene_delta) = frontend
10000 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line3_id])
10001 .await
10002 .unwrap();
10003 assert_eq!(
10004 src_delta.text.as_str(),
10005 "\
10006sketch(on = XY) {
10007 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10008 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10009 parallel([line1, line2])
10010}
10011"
10012 );
10013
10014 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10015 let sketch = expect_sketch(sketch_object);
10016 assert_eq!(sketch.constraints.len(), 1);
10017
10018 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10019 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10020 panic!("Expected constraint object");
10021 };
10022 let Constraint::Parallel(parallel) = constraint else {
10023 panic!("Expected parallel constraint");
10024 };
10025 assert_eq!(parallel.lines.len(), 2);
10026
10027 ctx.close().await;
10028 mock_ctx.close().await;
10029 }
10030
10031 #[tokio::test(flavor = "multi_thread")]
10032 async fn test_delete_lines_removes_multiline_parallel_constraint_below_minimum() {
10033 let initial_source = "\
10034sketch(on = XY) {
10035 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10036 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10037 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10038 parallel([line1, line2, line3])
10039}
10040";
10041
10042 let program = Program::parse(initial_source).unwrap().0.unwrap();
10043
10044 let mut frontend = FrontendState::new();
10045
10046 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10047 let mock_ctx = ExecutorContext::new_mock(None).await;
10048 let version = Version(0);
10049
10050 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10051 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10052 let sketch_id = sketch_object.id;
10053 let sketch = expect_sketch(sketch_object);
10054 let line2_id = *sketch.segments.get(5).unwrap();
10055 let line3_id = *sketch.segments.get(8).unwrap();
10056
10057 let (src_delta, scene_delta) = frontend
10058 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line2_id, line3_id])
10059 .await
10060 .unwrap();
10061 assert_eq!(
10062 src_delta.text.as_str(),
10063 "\
10064sketch(on = XY) {
10065 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10066}
10067"
10068 );
10069
10070 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10071 let sketch = expect_sketch(sketch_object);
10072 assert!(sketch.constraints.is_empty());
10073
10074 ctx.close().await;
10075 mock_ctx.close().await;
10076 }
10077
10078 #[tokio::test(flavor = "multi_thread")]
10079 async fn test_delete_line_line_coincident_constraint() {
10080 let initial_source = "\
10081sketch(on = XY) {
10082 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10083 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10084 coincident([line1, line2])
10085}
10086";
10087
10088 let program = Program::parse(initial_source).unwrap().0.unwrap();
10089
10090 let mut frontend = FrontendState::new();
10091
10092 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10093 let mock_ctx = ExecutorContext::new_mock(None).await;
10094 let version = Version(0);
10095
10096 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10097 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10098 let sketch_id = sketch_object.id;
10099 let sketch = expect_sketch(sketch_object);
10100
10101 let coincident_id = *sketch.constraints.first().unwrap();
10102
10103 let (src_delta, scene_delta) = frontend
10104 .delete_objects(&mock_ctx, version, sketch_id, vec![coincident_id], Vec::new())
10105 .await
10106 .unwrap();
10107 assert_eq!(
10108 src_delta.text.as_str(),
10109 "\
10110sketch(on = XY) {
10111 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10112 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10113}
10114"
10115 );
10116 assert_eq!(scene_delta.new_objects, vec![]);
10117 assert_eq!(scene_delta.new_graph.objects.len(), 8);
10118
10119 ctx.close().await;
10120 mock_ctx.close().await;
10121 }
10122
10123 #[tokio::test(flavor = "multi_thread")]
10124 async fn test_two_points_coincident() {
10125 let initial_source = "\
10126sketch(on = XY) {
10127 point1 = point(at = [var 1, var 2])
10128 point(at = [3, 4])
10129}
10130";
10131
10132 let program = Program::parse(initial_source).unwrap().0.unwrap();
10133
10134 let mut frontend = FrontendState::new();
10135
10136 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10137 let mock_ctx = ExecutorContext::new_mock(None).await;
10138 let version = Version(0);
10139
10140 frontend.hack_set_program(&ctx, program).await.unwrap();
10141 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10142 let sketch_id = sketch_object.id;
10143 let sketch = expect_sketch(sketch_object);
10144 let point0_id = *sketch.segments.first().unwrap();
10145 let point1_id = *sketch.segments.get(1).unwrap();
10146
10147 let constraint = Constraint::Coincident(Coincident {
10148 segments: vec![point0_id.into(), point1_id.into()],
10149 });
10150 let (src_delta, scene_delta) = frontend
10151 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10152 .await
10153 .unwrap();
10154 assert_eq!(
10155 src_delta.text.as_str(),
10156 "\
10157sketch(on = XY) {
10158 point1 = point(at = [var 3, var 4])
10159 point2 = point(at = [3, 4])
10160 coincident([point1, point2])
10161}
10162"
10163 );
10164 assert_eq!(
10165 scene_delta.new_graph.objects.len(),
10166 5,
10167 "{:#?}",
10168 scene_delta.new_graph.objects
10169 );
10170
10171 ctx.close().await;
10172 mock_ctx.close().await;
10173 }
10174
10175 #[tokio::test(flavor = "multi_thread")]
10176 async fn test_three_points_coincident() {
10177 let initial_source = "\
10178sketch(on = XY) {
10179 point1 = point(at = [var 1, var 2])
10180 point(at = [var 3, var 4])
10181 point(at = [var 5, var 6])
10182}
10183";
10184
10185 let program = Program::parse(initial_source).unwrap().0.unwrap();
10186
10187 let mut frontend = FrontendState::new();
10188
10189 let mock_ctx = ExecutorContext::new_mock(None).await;
10190 let version = Version(0);
10191
10192 frontend.program = program.clone();
10193 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10194 frontend.update_state_after_exec(outcome, true);
10195 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10196 let sketch_id = sketch_object.id;
10197 let sketch = expect_sketch(sketch_object);
10198 let segments = sketch
10199 .segments
10200 .iter()
10201 .take(3)
10202 .copied()
10203 .map(Into::into)
10204 .collect::<Vec<ConstraintSegment>>();
10205
10206 let constraint = Constraint::Coincident(Coincident {
10207 segments: segments.clone(),
10208 });
10209 let (src_delta, scene_delta) = frontend
10210 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10211 .await
10212 .unwrap();
10213 assert_eq!(
10214 src_delta.text.as_str(),
10215 "\
10216sketch(on = XY) {
10217 point1 = point(at = [var 3, var 4])
10218 point2 = point(at = [var 3, var 4])
10219 point3 = point(at = [var 3, var 4])
10220 coincident([point1, point2, point3])
10221}
10222"
10223 );
10224
10225 let constraint_object = scene_delta
10226 .new_graph
10227 .objects
10228 .iter()
10229 .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
10230 .unwrap();
10231
10232 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10233 panic!("expected a constraint object");
10234 };
10235
10236 assert_eq!(constraint, &Constraint::Coincident(Coincident { segments }));
10237
10238 mock_ctx.close().await;
10239 }
10240
10241 #[tokio::test(flavor = "multi_thread")]
10242 async fn test_source_with_three_point_coincident_tracks_all_segments() {
10243 let initial_source = "\
10244sketch(on = XY) {
10245 point1 = point(at = [var 1, var 2])
10246 point2 = point(at = [var 3, var 4])
10247 point3 = point(at = [var 5, var 6])
10248 coincident([point1, point2, point3])
10249}
10250";
10251
10252 let program = Program::parse(initial_source).unwrap().0.unwrap();
10253
10254 let mut frontend = FrontendState::new();
10255
10256 let ctx = ExecutorContext::new_mock(None).await;
10257 frontend.program = program.clone();
10258 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10259 frontend.update_state_after_exec(outcome, true);
10260
10261 let constraint_object = frontend
10262 .scene_graph
10263 .objects
10264 .iter()
10265 .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
10266 .unwrap();
10267 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10268 panic!("expected a constraint object");
10269 };
10270
10271 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10272 let sketch = expect_sketch(sketch_object);
10273 let expected_segments = sketch
10274 .segments
10275 .iter()
10276 .take(3)
10277 .copied()
10278 .map(Into::into)
10279 .collect::<Vec<ConstraintSegment>>();
10280
10281 assert_eq!(
10282 constraint,
10283 &Constraint::Coincident(Coincident {
10284 segments: expected_segments,
10285 })
10286 );
10287
10288 ctx.close().await;
10289 }
10290
10291 #[tokio::test(flavor = "multi_thread")]
10292 async fn test_point_origin_coincident_preserves_order() {
10293 let initial_source = "\
10294sketch(on = XY) {
10295 point(at = [var 1, var 2])
10296}
10297";
10298
10299 for (origin_first, expected_source) in [
10300 (
10301 true,
10302 "\
10303sketch(on = XY) {
10304 point1 = point(at = [var 0, var 0])
10305 coincident([ORIGIN, point1])
10306}
10307",
10308 ),
10309 (
10310 false,
10311 "\
10312sketch(on = XY) {
10313 point1 = point(at = [var 0, var 0])
10314 coincident([point1, ORIGIN])
10315}
10316",
10317 ),
10318 ] {
10319 let program = Program::parse(initial_source).unwrap().0.unwrap();
10320
10321 let mut frontend = FrontendState::new();
10322
10323 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10324 let mock_ctx = ExecutorContext::new_mock(None).await;
10325 let version = Version(0);
10326
10327 frontend.hack_set_program(&ctx, program).await.unwrap();
10328 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10329 let sketch_id = sketch_object.id;
10330 let sketch = expect_sketch(sketch_object);
10331 let point_id = *sketch.segments.first().unwrap();
10332
10333 let segments = if origin_first {
10334 vec![ConstraintSegment::ORIGIN, point_id.into()]
10335 } else {
10336 vec![point_id.into(), ConstraintSegment::ORIGIN]
10337 };
10338 let constraint = Constraint::Coincident(Coincident {
10339 segments: segments.clone(),
10340 });
10341 let (src_delta, scene_delta) = frontend
10342 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10343 .await
10344 .unwrap();
10345 assert_eq!(src_delta.text.as_str(), expected_source);
10346
10347 let constraint_object = scene_delta
10348 .new_graph
10349 .objects
10350 .iter()
10351 .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
10352 .unwrap();
10353
10354 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10355 panic!("expected a constraint object");
10356 };
10357
10358 assert_eq!(constraint, &Constraint::Coincident(Coincident { segments }));
10359
10360 ctx.close().await;
10361 mock_ctx.close().await;
10362 }
10363 }
10364
10365 #[tokio::test(flavor = "multi_thread")]
10366 async fn test_coincident_of_line_end_points() {
10367 let initial_source = "\
10368sketch(on = XY) {
10369 line(start = [var 1, var 2], end = [var 3, var 4])
10370 line(start = [var 5, var 6], end = [var 7, var 8])
10371}
10372";
10373
10374 let program = Program::parse(initial_source).unwrap().0.unwrap();
10375
10376 let mut frontend = FrontendState::new();
10377
10378 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10379 let mock_ctx = ExecutorContext::new_mock(None).await;
10380 let version = Version(0);
10381
10382 frontend.hack_set_program(&ctx, program).await.unwrap();
10383 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10384 let sketch_id = sketch_object.id;
10385 let sketch = expect_sketch(sketch_object);
10386 let point0_id = *sketch.segments.get(1).unwrap();
10387 let point1_id = *sketch.segments.get(3).unwrap();
10388
10389 let constraint = Constraint::Coincident(Coincident {
10390 segments: vec![point0_id.into(), point1_id.into()],
10391 });
10392 let (src_delta, scene_delta) = frontend
10393 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10394 .await
10395 .unwrap();
10396 assert_eq!(
10397 src_delta.text.as_str(),
10398 "\
10399sketch(on = XY) {
10400 line1 = line(start = [var 1, var 2], end = [var 4, var 5])
10401 line2 = line(start = [var 4, var 5], end = [var 7, var 8])
10402 coincident([line1.end, line2.start])
10403}
10404"
10405 );
10406 assert_eq!(
10407 scene_delta.new_graph.objects.len(),
10408 9,
10409 "{:#?}",
10410 scene_delta.new_graph.objects
10411 );
10412
10413 ctx.close().await;
10414 mock_ctx.close().await;
10415 }
10416
10417 #[tokio::test(flavor = "multi_thread")]
10418 async fn test_coincident_of_line_point_and_circle_segment() {
10419 let initial_source = "\
10420sketch(on = XY) {
10421 circle1 = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
10422 line1 = line(start = [var 9mm, var 1mm], end = [var 10mm, var 2mm])
10423}
10424";
10425 let program = Program::parse(initial_source).unwrap().0.unwrap();
10426 let mut frontend = FrontendState::new();
10427
10428 let mock_ctx = ExecutorContext::new_mock(None).await;
10429 let version = Version(0);
10430
10431 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10432 frontend.program = program;
10433 frontend.update_state_after_exec(outcome, true);
10434 let sketch_object = find_first_sketch_object(&frontend.scene_graph).expect("Expected sketch object");
10435 let sketch_id = sketch_object.id;
10436 let sketch = expect_sketch(sketch_object);
10437
10438 let circle_id = sketch
10439 .segments
10440 .iter()
10441 .copied()
10442 .find(|seg_id| {
10443 matches!(
10444 &frontend.scene_graph.objects[seg_id.0].kind,
10445 ObjectKind::Segment {
10446 segment: Segment::Circle(_)
10447 }
10448 )
10449 })
10450 .expect("Expected a circle segment in sketch");
10451 let line_id = frontend
10452 .scene_graph
10453 .objects
10454 .iter()
10455 .find_map(|obj| match &obj.kind {
10456 ObjectKind::Segment {
10457 segment: Segment::Line(line),
10458 } if line.owner.is_none() => Some(obj.id),
10459 _ => None,
10460 })
10461 .expect("Expected a standalone line segment in scene graph");
10462
10463 let line_start_point_id = match &frontend.scene_graph.objects[line_id.0].kind {
10464 ObjectKind::Segment {
10465 segment: Segment::Line(line),
10466 } => line.start,
10467 _ => panic!("Expected line segment object"),
10468 };
10469
10470 let constraint = Constraint::Coincident(Coincident {
10471 segments: vec![line_start_point_id.into(), circle_id.into()],
10472 });
10473 let (src_delta, _scene_delta) = frontend
10474 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10475 .await
10476 .unwrap();
10477 assert_eq!(
10478 src_delta.text.as_str(),
10479 "\
10480sketch(on = XY) {
10481 circle1 = circle(start = [var 7.02mm, var 0mm], center = [var -0.01mm, var 0.22mm])
10482 line1 = line(start = [var 7mm, var 0.78mm], end = [var 10mm, var 2mm])
10483 coincident([line1.start, circle1])
10484}
10485"
10486 );
10487
10488 mock_ctx.close().await;
10489 }
10490
10491 #[tokio::test(flavor = "multi_thread")]
10492 async fn test_invalid_coincident_arc_and_line_preserves_state() {
10493 let program = Program::empty();
10501
10502 let mut frontend = FrontendState::new();
10503 frontend.program = program;
10504
10505 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10506 let mock_ctx = ExecutorContext::new_mock(None).await;
10507 let version = Version(0);
10508
10509 let sketch_args = SketchCtor {
10510 on: Plane::Default(PlaneName::Xy),
10511 };
10512 let (_src_delta, _scene_delta, sketch_id) = frontend
10513 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
10514 .await
10515 .unwrap();
10516
10517 let arc_ctor = ArcCtor {
10519 start: Point2d {
10520 x: Expr::Var(Number {
10521 value: 0.0,
10522 units: NumericSuffix::Mm,
10523 }),
10524 y: Expr::Var(Number {
10525 value: 0.0,
10526 units: NumericSuffix::Mm,
10527 }),
10528 },
10529 end: Point2d {
10530 x: Expr::Var(Number {
10531 value: 10.0,
10532 units: NumericSuffix::Mm,
10533 }),
10534 y: Expr::Var(Number {
10535 value: 10.0,
10536 units: NumericSuffix::Mm,
10537 }),
10538 },
10539 center: Point2d {
10540 x: Expr::Var(Number {
10541 value: 10.0,
10542 units: NumericSuffix::Mm,
10543 }),
10544 y: Expr::Var(Number {
10545 value: 0.0,
10546 units: NumericSuffix::Mm,
10547 }),
10548 },
10549 construction: None,
10550 };
10551 let (_src_delta, scene_delta) = frontend
10552 .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Arc(arc_ctor), None)
10553 .await
10554 .unwrap();
10555 let arc_id = *scene_delta.new_objects.last().unwrap();
10557
10558 let line_ctor = LineCtor {
10560 start: Point2d {
10561 x: Expr::Var(Number {
10562 value: 20.0,
10563 units: NumericSuffix::Mm,
10564 }),
10565 y: Expr::Var(Number {
10566 value: 0.0,
10567 units: NumericSuffix::Mm,
10568 }),
10569 },
10570 end: Point2d {
10571 x: Expr::Var(Number {
10572 value: 30.0,
10573 units: NumericSuffix::Mm,
10574 }),
10575 y: Expr::Var(Number {
10576 value: 10.0,
10577 units: NumericSuffix::Mm,
10578 }),
10579 },
10580 construction: None,
10581 };
10582 let (_src_delta, scene_delta) = frontend
10583 .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Line(line_ctor), None)
10584 .await
10585 .unwrap();
10586 let line_id = *scene_delta.new_objects.last().unwrap();
10588
10589 let constraint = Constraint::Coincident(Coincident {
10592 segments: vec![arc_id.into(), line_id.into()],
10593 });
10594 let result = frontend.add_constraint(&mock_ctx, version, sketch_id, constraint).await;
10595
10596 assert!(result.is_err(), "Expected invalid coincident constraint to fail");
10598
10599 let sketch_object_after =
10602 find_first_sketch_object(&frontend.scene_graph).expect("Sketch should still exist after failed constraint");
10603 let sketch_after = expect_sketch(sketch_object_after);
10604
10605 assert!(
10607 sketch_after.segments.contains(&arc_id),
10608 "Arc segment should still exist after failed constraint"
10609 );
10610 assert!(
10611 sketch_after.segments.contains(&line_id),
10612 "Line segment should still exist after failed constraint"
10613 );
10614
10615 let arc_obj = frontend
10617 .scene_graph
10618 .objects
10619 .get(arc_id.0)
10620 .expect("Arc object should still be accessible");
10621 let line_obj = frontend
10622 .scene_graph
10623 .objects
10624 .get(line_id.0)
10625 .expect("Line object should still be accessible");
10626
10627 match &arc_obj.kind {
10630 ObjectKind::Segment {
10631 segment: Segment::Arc(_),
10632 } => {}
10633 _ => panic!("Arc object should still be an arc segment"),
10634 }
10635 match &line_obj.kind {
10636 ObjectKind::Segment {
10637 segment: Segment::Line(_),
10638 } => {}
10639 _ => panic!("Line object should still be a line segment"),
10640 }
10641
10642 ctx.close().await;
10643 mock_ctx.close().await;
10644 }
10645
10646 #[tokio::test(flavor = "multi_thread")]
10647 async fn test_distance_two_points() {
10648 let initial_source = "\
10649sketch(on = XY) {
10650 point(at = [var 1, var 2])
10651 point(at = [var 3, var 4])
10652}
10653";
10654
10655 let program = Program::parse(initial_source).unwrap().0.unwrap();
10656
10657 let mut frontend = FrontendState::new();
10658
10659 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10660 let mock_ctx = ExecutorContext::new_mock(None).await;
10661 let version = Version(0);
10662
10663 frontend.hack_set_program(&ctx, program).await.unwrap();
10664 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10665 let sketch_id = sketch_object.id;
10666 let sketch = expect_sketch(sketch_object);
10667 let point0_id = *sketch.segments.first().unwrap();
10668 let point1_id = *sketch.segments.get(1).unwrap();
10669
10670 let constraint = Constraint::Distance(Distance {
10671 points: vec![point0_id.into(), point1_id.into()],
10672 distance: Number {
10673 value: 2.0,
10674 units: NumericSuffix::Mm,
10675 },
10676 label_position: None,
10677 source: Default::default(),
10678 });
10679 let (src_delta, scene_delta) = frontend
10680 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10681 .await
10682 .unwrap();
10683 assert_eq!(
10684 src_delta.text.as_str(),
10685 "\
10687sketch(on = XY) {
10688 point1 = point(at = [var 1.29, var 2.29])
10689 point2 = point(at = [var 2.71, var 3.71])
10690 distance([point1, point2]) == 2mm
10691}
10692"
10693 );
10694 assert_eq!(
10695 scene_delta.new_graph.objects.len(),
10696 5,
10697 "{:#?}",
10698 scene_delta.new_graph.objects
10699 );
10700
10701 ctx.close().await;
10702 mock_ctx.close().await;
10703 }
10704
10705 #[tokio::test(flavor = "multi_thread")]
10706 async fn test_distance_two_points_with_label() {
10707 let initial_source = "\
10708sketch(on = XY) {
10709 point(at = [var 1, var 2])
10710 point(at = [var 3, var 4])
10711}
10712";
10713
10714 let program = Program::parse(initial_source).unwrap().0.unwrap();
10715
10716 let mut frontend = FrontendState::new();
10717
10718 let mock_ctx = ExecutorContext::new_mock(None).await;
10719 let version = Version(0);
10720
10721 frontend.program = program.clone();
10722 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10723 frontend.update_state_after_exec(outcome, true);
10724 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10725 let sketch_id = sketch_object.id;
10726 let sketch = expect_sketch(sketch_object);
10727 let point0_id = *sketch.segments.first().unwrap();
10728 let point1_id = *sketch.segments.get(1).unwrap();
10729
10730 let label_position = Point2d {
10731 x: Number {
10732 value: 10.0,
10733 units: NumericSuffix::Mm,
10734 },
10735 y: Number {
10736 value: 11.0,
10737 units: NumericSuffix::Mm,
10738 },
10739 };
10740 let constraint = Constraint::Distance(Distance {
10741 points: vec![point0_id.into(), point1_id.into()],
10742 distance: Number {
10743 value: 2.0,
10744 units: NumericSuffix::Mm,
10745 },
10746 label_position: Some(label_position.clone()),
10747 source: Default::default(),
10748 });
10749 let (src_delta, scene_delta) = frontend
10750 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10751 .await
10752 .unwrap();
10753 assert_eq!(
10754 src_delta.text.as_str(),
10755 "\
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], labelPosition = [10mm, 11mm]) == 2mm
10760}
10761"
10762 );
10763
10764 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10765 let sketch = expect_sketch(sketch_object);
10766 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10767 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10768 panic!("Expected constraint object");
10769 };
10770 let Constraint::Distance(distance) = constraint else {
10771 panic!("Expected distance constraint");
10772 };
10773 assert_eq!(distance.label_position, Some(label_position));
10774
10775 mock_ctx.close().await;
10776 }
10777
10778 #[tokio::test(flavor = "multi_thread")]
10779 async fn test_edit_distance_constraint_label_position() {
10780 let initial_source = "\
10781sketch(on = XY) {
10782 point(at = [var 1, var 2])
10783 point(at = [var 3, var 2])
10784}
10785";
10786
10787 let program = Program::parse(initial_source).unwrap().0.unwrap();
10788
10789 let mut frontend = FrontendState::new();
10790
10791 let mock_ctx = ExecutorContext::new_mock(None).await;
10792 let version = Version(0);
10793
10794 frontend.program = program.clone();
10795 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10796 frontend.update_state_after_exec(outcome, true);
10797 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10798 let sketch_id = sketch_object.id;
10799 let sketch = expect_sketch(sketch_object);
10800 let point0_id = *sketch.segments.first().unwrap();
10801 let point1_id = *sketch.segments.get(1).unwrap();
10802
10803 let constraint = Constraint::Distance(Distance {
10804 points: vec![point0_id.into(), point1_id.into()],
10805 distance: Number {
10806 value: 2.0,
10807 units: NumericSuffix::Mm,
10808 },
10809 label_position: None,
10810 source: Default::default(),
10811 });
10812 let (_, scene_delta) = frontend
10813 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10814 .await
10815 .unwrap();
10816 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10817 let sketch = expect_sketch(sketch_object);
10818 let constraint_id = sketch.constraints[0];
10819 let label_position = Point2d {
10820 x: Number {
10821 value: 10.0,
10822 units: NumericSuffix::Mm,
10823 },
10824 y: Number {
10825 value: 11.0,
10826 units: NumericSuffix::Mm,
10827 },
10828 };
10829
10830 let (src_delta, scene_delta) = frontend
10831 .edit_distance_constraint_label_position(
10832 &mock_ctx,
10833 version,
10834 sketch_id,
10835 constraint_id,
10836 label_position.clone(),
10837 vec![],
10838 )
10839 .await
10840 .unwrap();
10841 assert_eq!(
10842 src_delta.text.as_str(),
10843 "\
10844sketch(on = XY) {
10845 point1 = point(at = [var 1, var 2])
10846 point2 = point(at = [var 3, var 2])
10847 distance([point1, point2], labelPosition = [10mm, 11mm]) == 2mm
10848}
10849"
10850 );
10851
10852 let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
10853 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10854 panic!("Expected constraint object");
10855 };
10856 let Constraint::Distance(distance) = constraint else {
10857 panic!("Expected distance constraint");
10858 };
10859 assert_eq!(distance.label_position, Some(label_position));
10860
10861 mock_ctx.close().await;
10862 }
10863
10864 #[tokio::test(flavor = "multi_thread")]
10865 async fn test_edit_distance_constraint_label_position_preserves_anchor_segment_solution() {
10866 let initial_source = "\
10867sketch(on = XY) {
10868 point1 = point(at = [var 0mm, var 0mm])
10869 point2 = point(at = [var 10mm, var 0mm])
10870 distance([point1, point2]) == 5mm
10871}
10872";
10873
10874 let program = Program::parse(initial_source).unwrap().0.unwrap();
10875 let mut frontend = FrontendState::new();
10876 let mock_ctx = ExecutorContext::new_mock(None).await;
10877 let version = Version(0);
10878
10879 frontend.program = program.clone();
10880 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10881 frontend.update_state_after_exec(outcome, true);
10882 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10883 let sketch_id = sketch_object.id;
10884 let sketch = expect_sketch(sketch_object);
10885 let point0_id = sketch.segments[0];
10886 let point1_id = sketch.segments[1];
10887 let constraint_id = sketch.constraints[0];
10888
10889 let edited_segments = vec![ExistingSegmentCtor {
10890 id: point0_id,
10891 ctor: SegmentCtor::Point(PointCtor {
10892 position: Point2d {
10893 x: Expr::Var(Number {
10894 value: 2.0,
10895 units: NumericSuffix::Mm,
10896 }),
10897 y: Expr::Var(Number {
10898 value: 1.0,
10899 units: NumericSuffix::Mm,
10900 }),
10901 },
10902 }),
10903 }];
10904 let (_, scene_delta) = frontend
10905 .edit_segments(&mock_ctx, version, sketch_id, edited_segments)
10906 .await
10907 .unwrap();
10908 let point0_after_segment_edit = point_position(&scene_delta.new_graph, point0_id);
10909 let point1_after_segment_edit = point_position(&scene_delta.new_graph, point1_id);
10910
10911 let label_position = Point2d {
10912 x: Number {
10913 value: 3.0,
10914 units: NumericSuffix::Mm,
10915 },
10916 y: Number {
10917 value: 4.0,
10918 units: NumericSuffix::Mm,
10919 },
10920 };
10921 let (_, scene_delta) = frontend
10922 .edit_distance_constraint_label_position(
10923 &mock_ctx,
10924 version,
10925 sketch_id,
10926 constraint_id,
10927 label_position,
10928 vec![point0_id],
10929 )
10930 .await
10931 .unwrap();
10932
10933 assert_point_position_close(
10934 point_position(&scene_delta.new_graph, point0_id),
10935 point0_after_segment_edit,
10936 );
10937 assert_point_position_close(
10938 point_position(&scene_delta.new_graph, point1_id),
10939 point1_after_segment_edit,
10940 );
10941
10942 mock_ctx.close().await;
10943 }
10944
10945 #[tokio::test(flavor = "multi_thread")]
10946 async fn test_distance_point_line() {
10947 let initial_source = "\
10948sketch(on = XY) {
10949 point(at = [var 0, var 5])
10950 line(start = [var 0, var 0], end = [var 10, var 0])
10951}
10952";
10953
10954 let program = Program::parse(initial_source).unwrap().0.unwrap();
10955
10956 let mut frontend = FrontendState::new();
10957
10958 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10959 let mock_ctx = ExecutorContext::new_mock(None).await;
10960 let version = Version(0);
10961
10962 frontend.hack_set_program(&ctx, program).await.unwrap();
10963 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10964 let sketch_id = sketch_object.id;
10965 let sketch = expect_sketch(sketch_object);
10966 let point_id = *sketch.segments.first().unwrap();
10967 let line_id = *sketch
10968 .segments
10969 .iter()
10970 .find(|segment_id| {
10971 matches!(
10972 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
10973 Some(ObjectKind::Segment {
10974 segment: Segment::Line(_)
10975 })
10976 )
10977 })
10978 .unwrap();
10979
10980 let label_position = Point2d {
10981 x: Number {
10982 value: 10.0,
10983 units: NumericSuffix::Mm,
10984 },
10985 y: Number {
10986 value: 11.0,
10987 units: NumericSuffix::Mm,
10988 },
10989 };
10990 let constraint = Constraint::Distance(Distance {
10991 points: vec![point_id.into(), line_id.into()],
10992 distance: Number {
10993 value: 5.0,
10994 units: NumericSuffix::Mm,
10995 },
10996 label_position: Some(label_position.clone()),
10997 source: Default::default(),
10998 });
10999 let (src_delta, scene_delta) = frontend
11000 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11001 .await
11002 .unwrap();
11003 assert_eq!(
11004 src_delta.text.as_str(),
11005 "\
11006sketch(on = XY) {
11007 point1 = point(at = [var 0, var 5])
11008 line1 = line(start = [var 0, var 0], end = [var 10, var 0])
11009 distance([point1, line1], labelPosition = [10mm, 11mm]) == 5mm
11010}
11011"
11012 );
11013 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
11014 let sketch = expect_sketch(sketch_object);
11015 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
11016 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11017 panic!("Expected constraint object");
11018 };
11019 let Constraint::Distance(distance) = constraint else {
11020 panic!("Expected distance constraint");
11021 };
11022 assert_eq!(distance.label_position, Some(label_position));
11023
11024 ctx.close().await;
11025 mock_ctx.close().await;
11026 }
11027
11028 #[tokio::test(flavor = "multi_thread")]
11029 async fn test_distance_point_arc() {
11030 let initial_source = "\
11031sketch(on = XY) {
11032 point(at = [var 0, var 8])
11033 arc(start = [var 5, var 0], end = [var 0, var 5], center = [var 0, var 0])
11034}
11035";
11036
11037 let program = Program::parse(initial_source).unwrap().0.unwrap();
11038
11039 let mut frontend = FrontendState::new();
11040
11041 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11042 let mock_ctx = ExecutorContext::new_mock(None).await;
11043 let version = Version(0);
11044
11045 frontend.hack_set_program(&ctx, program).await.unwrap();
11046 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11047 let sketch_id = sketch_object.id;
11048 let sketch = expect_sketch(sketch_object);
11049 let point_id = *sketch.segments.first().unwrap();
11050 let arc_id = *sketch
11051 .segments
11052 .iter()
11053 .find(|segment_id| {
11054 matches!(
11055 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11056 Some(ObjectKind::Segment {
11057 segment: Segment::Arc(_)
11058 })
11059 )
11060 })
11061 .unwrap();
11062
11063 let constraint = Constraint::Distance(Distance {
11064 points: vec![point_id.into(), arc_id.into()],
11065 distance: Number {
11066 value: 3.0,
11067 units: NumericSuffix::Mm,
11068 },
11069 label_position: None,
11070 source: Default::default(),
11071 });
11072 let (src_delta, _scene_delta) = frontend
11073 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11074 .await
11075 .unwrap();
11076 assert_eq!(
11077 src_delta.text.as_str(),
11078 "\
11079sketch(on = XY) {
11080 point1 = point(at = [var 0, var 8])
11081 arc1 = arc(start = [var 5, var 0], end = [var 0, var 5], center = [var 0, var 0])
11082 distance([point1, arc1]) == 3mm
11083}
11084"
11085 );
11086
11087 ctx.close().await;
11088 mock_ctx.close().await;
11089 }
11090
11091 #[tokio::test(flavor = "multi_thread")]
11092 async fn test_distance_arc_origin() {
11093 let initial_source = "\
11094sketch001 = sketch(on = XY) {
11095 arc(start = [var -4.13mm, var -0.59mm], end = [var -3.47mm, var 3.38mm], center = [var -4.55mm, var 1.52mm])
11096}
11097";
11098
11099 let program = Program::parse(initial_source).unwrap().0.unwrap();
11100
11101 let mut frontend = FrontendState::new();
11102
11103 let mock_ctx = ExecutorContext::new_mock(None).await;
11104 let version = Version(0);
11105
11106 frontend.program = program.clone();
11107 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11108 frontend.update_state_after_exec(outcome, true);
11109 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11110 let sketch_id = sketch_object.id;
11111 let sketch = expect_sketch(sketch_object);
11112 let arc_id = *sketch
11113 .segments
11114 .iter()
11115 .find(|segment_id| {
11116 matches!(
11117 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11118 Some(ObjectKind::Segment {
11119 segment: Segment::Arc(_)
11120 })
11121 )
11122 })
11123 .unwrap();
11124
11125 let constraint = Constraint::Distance(Distance {
11126 points: vec![arc_id.into(), ConstraintSegment::ORIGIN],
11127 distance: Number {
11128 value: 3.0,
11129 units: NumericSuffix::Mm,
11130 },
11131 label_position: None,
11132 source: Default::default(),
11133 });
11134 let (src_delta, _scene_delta) = frontend
11135 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11136 .await
11137 .unwrap();
11138 assert_eq!(
11139 src_delta.text.as_str(),
11140 "\
11141sketch001 = sketch(on = XY) {
11142 arc1 = arc(start = [var -4.16mm, var -0.43mm], end = [var -3.53mm, var 3.28mm], center = [var -4.91mm, var 1.61mm])
11143 distance([arc1, ORIGIN]) == 3mm
11144}
11145"
11146 );
11147
11148 mock_ctx.close().await;
11149 }
11150
11151 #[tokio::test(flavor = "multi_thread")]
11152 async fn test_distance_line_origin() {
11153 let initial_source = "\
11154sketch(on = XY) {
11155 line(start = [var 5, var 0], end = [var 5, var 10])
11156}
11157";
11158
11159 let program = Program::parse(initial_source).unwrap().0.unwrap();
11160
11161 let mut frontend = FrontendState::new();
11162
11163 let mock_ctx = ExecutorContext::new_mock(None).await;
11164 let version = Version(0);
11165
11166 frontend.program = program.clone();
11167 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11168 frontend.update_state_after_exec(outcome, true);
11169 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11170 let sketch_id = sketch_object.id;
11171 let sketch = expect_sketch(sketch_object);
11172 let line_id = *sketch
11173 .segments
11174 .iter()
11175 .find(|segment_id| {
11176 matches!(
11177 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11178 Some(ObjectKind::Segment {
11179 segment: Segment::Line(_)
11180 })
11181 )
11182 })
11183 .unwrap();
11184
11185 let constraint = Constraint::Distance(Distance {
11186 points: vec![ConstraintSegment::ORIGIN, line_id.into()],
11187 distance: Number {
11188 value: 5.0,
11189 units: NumericSuffix::Mm,
11190 },
11191 label_position: None,
11192 source: Default::default(),
11193 });
11194 let (src_delta, _scene_delta) = frontend
11195 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11196 .await
11197 .unwrap();
11198 assert_eq!(
11199 src_delta.text.as_str(),
11200 "\
11201sketch(on = XY) {
11202 line1 = line(start = [var 5, var 0], end = [var 5, var 10])
11203 distance([ORIGIN, line1]) == 5mm
11204}
11205"
11206 );
11207
11208 mock_ctx.close().await;
11209 }
11210
11211 #[tokio::test(flavor = "multi_thread")]
11212 async fn test_distance_line_circle() {
11213 let initial_source = "\
11214sketch(on = XY) {
11215 line(start = [var -10, var 8], end = [var 10, var 8])
11216 circle(start = [var 5, var 0], center = [var 0, var 0])
11217}
11218";
11219
11220 let program = Program::parse(initial_source).unwrap().0.unwrap();
11221
11222 let mut frontend = FrontendState::new();
11223
11224 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11225 let mock_ctx = ExecutorContext::new_mock(None).await;
11226 let version = Version(0);
11227
11228 frontend.hack_set_program(&ctx, program).await.unwrap();
11229 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11230 let sketch_id = sketch_object.id;
11231 let sketch = expect_sketch(sketch_object);
11232 let line_id = *sketch
11233 .segments
11234 .iter()
11235 .find(|segment_id| {
11236 matches!(
11237 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11238 Some(ObjectKind::Segment {
11239 segment: Segment::Line(_)
11240 })
11241 )
11242 })
11243 .unwrap();
11244 let circle_id = *sketch
11245 .segments
11246 .iter()
11247 .find(|segment_id| {
11248 matches!(
11249 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11250 Some(ObjectKind::Segment {
11251 segment: Segment::Circle(_)
11252 })
11253 )
11254 })
11255 .unwrap();
11256
11257 let constraint = Constraint::Distance(Distance {
11258 points: vec![line_id.into(), circle_id.into()],
11259 distance: Number {
11260 value: 3.0,
11261 units: NumericSuffix::Mm,
11262 },
11263 label_position: None,
11264 source: Default::default(),
11265 });
11266 let (src_delta, _scene_delta) = frontend
11267 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11268 .await
11269 .unwrap();
11270 assert_eq!(
11271 src_delta.text.as_str(),
11272 "\
11273sketch(on = XY) {
11274 line1 = line(start = [var -10, var 8], end = [var 10, var 8])
11275 circle1 = circle(start = [var 5, var 0], center = [var 0, var 0])
11276 distance([line1, circle1]) == 3mm
11277}
11278"
11279 );
11280
11281 ctx.close().await;
11282 mock_ctx.close().await;
11283 }
11284
11285 #[tokio::test(flavor = "multi_thread")]
11286 async fn test_distance_circle_arc() {
11287 let initial_source = "\
11288sketch(on = XY) {
11289 circle(start = [var 5, var 0], center = [var 0, var 0])
11290 arc(start = [var 15, var 0], end = [var 10, var 5], center = [var 10, var 0])
11291}
11292";
11293
11294 let program = Program::parse(initial_source).unwrap().0.unwrap();
11295
11296 let mut frontend = FrontendState::new();
11297
11298 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11299 let mock_ctx = ExecutorContext::new_mock(None).await;
11300 let version = Version(0);
11301
11302 frontend.hack_set_program(&ctx, program).await.unwrap();
11303 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11304 let sketch_id = sketch_object.id;
11305 let sketch = expect_sketch(sketch_object);
11306 let circle_id = *sketch
11307 .segments
11308 .iter()
11309 .find(|segment_id| {
11310 matches!(
11311 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11312 Some(ObjectKind::Segment {
11313 segment: Segment::Circle(_)
11314 })
11315 )
11316 })
11317 .unwrap();
11318 let arc_id = *sketch
11319 .segments
11320 .iter()
11321 .find(|segment_id| {
11322 matches!(
11323 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11324 Some(ObjectKind::Segment {
11325 segment: Segment::Arc(_)
11326 })
11327 )
11328 })
11329 .unwrap();
11330
11331 let constraint = Constraint::Distance(Distance {
11332 points: vec![circle_id.into(), arc_id.into()],
11333 distance: Number {
11334 value: 3.0,
11335 units: NumericSuffix::Mm,
11336 },
11337 label_position: None,
11338 source: Default::default(),
11339 });
11340 let (src_delta, _scene_delta) = frontend
11341 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11342 .await
11343 .unwrap();
11344 assert_eq!(
11345 src_delta.text.as_str(),
11346 "\
11347sketch(on = XY) {
11348 circle1 = circle(start = [var 4.33, var 0], center = [var -0.34, var -0.09])
11349 arc1 = arc(start = [var 15.33, var -0.01], end = [var 10.01, var 4.33], center = [var 11.34, var 0.53])
11350 distance([circle1, arc1]) == 3mm
11351}
11352"
11353 );
11354
11355 ctx.close().await;
11356 mock_ctx.close().await;
11357 }
11358
11359 #[tokio::test(flavor = "multi_thread")]
11360 async fn test_distance_parallel_lines() {
11361 let initial_source = "\
11362sketch(on = XY) {
11363 line(start = [var 0, var 0], end = [var 10, var 0])
11364 line(start = [var 0, var 5], end = [var 10, var 5])
11365}
11366";
11367
11368 let program = Program::parse(initial_source).unwrap().0.unwrap();
11369
11370 let mut frontend = FrontendState::new();
11371
11372 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11373 let mock_ctx = ExecutorContext::new_mock(None).await;
11374 let version = Version(0);
11375
11376 frontend.hack_set_program(&ctx, program).await.unwrap();
11377 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11378 let sketch_id = sketch_object.id;
11379 let sketch = expect_sketch(sketch_object);
11380 let line_ids = sketch
11381 .segments
11382 .iter()
11383 .copied()
11384 .filter(|segment_id| {
11385 matches!(
11386 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11387 Some(ObjectKind::Segment {
11388 segment: Segment::Line(_)
11389 })
11390 )
11391 })
11392 .collect::<Vec<_>>();
11393
11394 let constraint = Constraint::Distance(Distance {
11395 points: vec![line_ids[0].into(), line_ids[1].into()],
11396 distance: Number {
11397 value: 5.0,
11398 units: NumericSuffix::Mm,
11399 },
11400 label_position: None,
11401 source: Default::default(),
11402 });
11403 let (src_delta, _scene_delta) = frontend
11404 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11405 .await
11406 .unwrap();
11407 assert_eq!(
11408 src_delta.text.as_str(),
11409 "\
11410sketch(on = XY) {
11411 line1 = line(start = [var 0, var 0], end = [var 10, var 0])
11412 line2 = line(start = [var 0, var 5], end = [var 10, var 5])
11413 distance([line1, line2]) == 5mm
11414}
11415"
11416 );
11417
11418 ctx.close().await;
11419 mock_ctx.close().await;
11420 }
11421
11422 #[tokio::test(flavor = "multi_thread")]
11423 async fn test_distance_non_parallel_lines_lowers_to_distance() {
11424 let initial_source = "\
11425sketch(on = XY) {
11426 line(start = [var 0, var 0], end = [var 10, var 0])
11427 line(start = [var 0, var 0], end = [var 0, var 10])
11428}
11429";
11430
11431 let program = Program::parse(initial_source).unwrap().0.unwrap();
11432
11433 let mut frontend = FrontendState::new();
11434
11435 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11436 let mock_ctx = ExecutorContext::new_mock(None).await;
11437 let version = Version(0);
11438
11439 frontend.hack_set_program(&ctx, program).await.unwrap();
11440 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11441 let sketch_id = sketch_object.id;
11442 let sketch = expect_sketch(sketch_object);
11443 let line_ids = sketch
11444 .segments
11445 .iter()
11446 .copied()
11447 .filter(|segment_id| {
11448 matches!(
11449 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11450 Some(ObjectKind::Segment {
11451 segment: Segment::Line(_)
11452 })
11453 )
11454 })
11455 .collect::<Vec<_>>();
11456
11457 let constraint = Constraint::Distance(Distance {
11458 points: vec![line_ids[0].into(), line_ids[1].into()],
11459 distance: Number {
11460 value: 5.0,
11461 units: NumericSuffix::Mm,
11462 },
11463 label_position: None,
11464 source: Default::default(),
11465 });
11466 let (src_delta, _scene_delta) = frontend
11467 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11468 .await
11469 .unwrap();
11470 assert_eq!(
11471 src_delta.text.as_str(),
11472 "\
11473sketch(on = XY) {
11474 line1 = line(start = [var 4.98, var -0.07], end = [var 4.98, var 0.14])
11475 line2 = line(start = [var 0.02, var 4.3], end = [var 0.03, var 5.65])
11476 distance([line1, line2]) == 5mm
11477}
11478"
11479 );
11480
11481 ctx.close().await;
11482 mock_ctx.close().await;
11483 }
11484
11485 #[tokio::test(flavor = "multi_thread")]
11486 async fn test_horizontal_distance_two_points() {
11487 let initial_source = "\
11488sketch(on = XY) {
11489 point(at = [var 1, var 2])
11490 point(at = [var 3, var 4])
11491}
11492";
11493
11494 let program = Program::parse(initial_source).unwrap().0.unwrap();
11495
11496 let mut frontend = FrontendState::new();
11497
11498 let mock_ctx = ExecutorContext::new_mock(None).await;
11499 let version = Version(0);
11500
11501 frontend.program = program.clone();
11502 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11503 frontend.update_state_after_exec(outcome, true);
11504 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11505 let sketch_id = sketch_object.id;
11506 let sketch = expect_sketch(sketch_object);
11507 let point0_id = *sketch.segments.first().unwrap();
11508 let point1_id = *sketch.segments.get(1).unwrap();
11509 let label_position = Point2d {
11510 x: Number {
11511 value: 10.0,
11512 units: NumericSuffix::Mm,
11513 },
11514 y: Number {
11515 value: 11.0,
11516 units: NumericSuffix::Mm,
11517 },
11518 };
11519
11520 let constraint = Constraint::HorizontalDistance(Distance {
11521 points: vec![point0_id.into(), point1_id.into()],
11522 distance: Number {
11523 value: 2.0,
11524 units: NumericSuffix::Mm,
11525 },
11526 label_position: Some(label_position.clone()),
11527 source: Default::default(),
11528 });
11529 let (src_delta, scene_delta) = frontend
11530 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11531 .await
11532 .unwrap();
11533 assert_eq!(
11534 src_delta.text.as_str(),
11535 "\
11537sketch(on = XY) {
11538 point1 = point(at = [var 1, var 2])
11539 point2 = point(at = [var 3, var 4])
11540 horizontalDistance([point1, point2], labelPosition = [10mm, 11mm]) == 2mm
11541}
11542"
11543 );
11544 assert_eq!(
11545 scene_delta.new_graph.objects.len(),
11546 5,
11547 "{:#?}",
11548 scene_delta.new_graph.objects
11549 );
11550 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
11551 let sketch = expect_sketch(sketch_object);
11552 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
11553 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11554 panic!("Expected constraint object");
11555 };
11556 let Constraint::HorizontalDistance(distance) = constraint else {
11557 panic!("Expected horizontal distance constraint");
11558 };
11559 assert_eq!(distance.label_position, Some(label_position));
11560
11561 mock_ctx.close().await;
11562 }
11563
11564 #[tokio::test(flavor = "multi_thread")]
11565 async fn test_radius_single_arc_segment() {
11566 let initial_source = "\
11567sketch(on = XY) {
11568 arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
11569}
11570";
11571
11572 let program = Program::parse(initial_source).unwrap().0.unwrap();
11573
11574 let mut frontend = FrontendState::new();
11575
11576 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11577 let mock_ctx = ExecutorContext::new_mock(None).await;
11578 let version = Version(0);
11579
11580 frontend.hack_set_program(&ctx, program).await.unwrap();
11581 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11582 let sketch_id = sketch_object.id;
11583 let sketch = expect_sketch(sketch_object);
11584 let arc_id = sketch
11586 .segments
11587 .iter()
11588 .find(|&seg_id| {
11589 let obj = frontend.scene_graph.objects.get(seg_id.0);
11590 matches!(
11591 obj.map(|o| &o.kind),
11592 Some(ObjectKind::Segment {
11593 segment: Segment::Arc(_)
11594 })
11595 )
11596 })
11597 .unwrap();
11598
11599 let constraint = Constraint::Radius(Radius {
11600 arc: *arc_id,
11601 radius: Number {
11602 value: 5.0,
11603 units: NumericSuffix::Mm,
11604 },
11605 label_position: None,
11606 source: Default::default(),
11607 });
11608 let (src_delta, scene_delta) = frontend
11609 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11610 .await
11611 .unwrap();
11612 assert_eq!(
11613 src_delta.text.as_str(),
11614 "\
11616sketch(on = XY) {
11617 arc1 = arc(start = [var 1.83, var 3.62], end = [var 2.42, var 3.3], center = [var -0.25, var -0.92])
11618 radius(arc1) == 5mm
11619}
11620"
11621 );
11622 assert_eq!(
11623 scene_delta.new_graph.objects.len(),
11624 7, "{:#?}",
11626 scene_delta.new_graph.objects
11627 );
11628
11629 ctx.close().await;
11630 mock_ctx.close().await;
11631 }
11632
11633 #[tokio::test(flavor = "multi_thread")]
11634 async fn test_radius_single_arc_segment_with_label_position() {
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 let mut frontend = FrontendState::new();
11643 let mock_ctx = ExecutorContext::new_mock(None).await;
11644 let version = Version(0);
11645
11646 frontend.program = program.clone();
11647 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11648 frontend.update_state_after_exec(outcome, true);
11649 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11650 let sketch_id = sketch_object.id;
11651 let sketch = expect_sketch(sketch_object);
11652 let arc_id = sketch
11653 .segments
11654 .iter()
11655 .find(|&seg_id| {
11656 let obj = frontend.scene_graph.objects.get(seg_id.0);
11657 matches!(
11658 obj.map(|o| &o.kind),
11659 Some(ObjectKind::Segment {
11660 segment: Segment::Arc(_)
11661 })
11662 )
11663 })
11664 .unwrap();
11665
11666 let label_position = Point2d {
11667 x: Number {
11668 value: 10.0,
11669 units: NumericSuffix::Mm,
11670 },
11671 y: Number {
11672 value: 11.0,
11673 units: NumericSuffix::Mm,
11674 },
11675 };
11676 let constraint = Constraint::Radius(Radius {
11677 arc: *arc_id,
11678 radius: Number {
11679 value: 5.0,
11680 units: NumericSuffix::Mm,
11681 },
11682 label_position: Some(label_position.clone()),
11683 source: Default::default(),
11684 });
11685 let (src_delta, scene_delta) = frontend
11686 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11687 .await
11688 .unwrap();
11689 assert_eq!(
11690 src_delta.text.as_str(),
11691 "\
11692sketch(on = XY) {
11693 arc1 = arc(start = [var 1.83, var 3.62], end = [var 2.42, var 3.3], center = [var -0.25, var -0.92])
11694 radius(arc1, labelPosition = [10mm, 11mm]) == 5mm
11695}
11696"
11697 );
11698
11699 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
11700 let sketch = expect_sketch(sketch_object);
11701 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
11702 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11703 panic!("Expected constraint object");
11704 };
11705 let Constraint::Radius(radius) = constraint else {
11706 panic!("Expected radius constraint");
11707 };
11708 assert_eq!(radius.label_position, Some(label_position));
11709
11710 mock_ctx.close().await;
11711 }
11712
11713 #[tokio::test(flavor = "multi_thread")]
11714 async fn test_edit_radius_constraint_label_position() {
11715 let initial_source = "\
11716sketch(on = XY) {
11717 arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
11718 radius(arc1) == 5mm
11719}
11720";
11721
11722 let program = Program::parse(initial_source).unwrap().0.unwrap();
11723 let mut frontend = FrontendState::new();
11724 let mock_ctx = ExecutorContext::new_mock(None).await;
11725 let version = Version(0);
11726
11727 frontend.program = program.clone();
11728 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11729 frontend.update_state_after_exec(outcome, true);
11730 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11731 let sketch_id = sketch_object.id;
11732 let sketch = expect_sketch(sketch_object);
11733 let constraint_id = sketch.constraints[0];
11734 let label_position = Point2d {
11735 x: Number {
11736 value: 10.0,
11737 units: NumericSuffix::Mm,
11738 },
11739 y: Number {
11740 value: 11.0,
11741 units: NumericSuffix::Mm,
11742 },
11743 };
11744
11745 let (src_delta, scene_delta) = frontend
11746 .edit_distance_constraint_label_position(
11747 &mock_ctx,
11748 version,
11749 sketch_id,
11750 constraint_id,
11751 label_position.clone(),
11752 vec![],
11753 )
11754 .await
11755 .unwrap();
11756 assert_eq!(
11757 src_delta.text.as_str(),
11758 "\
11759sketch(on = XY) {
11760 arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
11761 radius(arc1, labelPosition = [10mm, 11mm]) == 5mm
11762}
11763"
11764 );
11765
11766 let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
11767 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11768 panic!("Expected constraint object");
11769 };
11770 let Constraint::Radius(radius) = constraint else {
11771 panic!("Expected radius constraint");
11772 };
11773 assert_eq!(radius.label_position, Some(label_position));
11774
11775 mock_ctx.close().await;
11776 }
11777
11778 #[tokio::test(flavor = "multi_thread")]
11779 async fn test_vertical_distance_two_points() {
11780 let initial_source = "\
11781sketch(on = XY) {
11782 point(at = [var 1, var 2])
11783 point(at = [var 3, var 4])
11784}
11785";
11786
11787 let program = Program::parse(initial_source).unwrap().0.unwrap();
11788
11789 let mut frontend = FrontendState::new();
11790
11791 let mock_ctx = ExecutorContext::new_mock(None).await;
11792 let version = Version(0);
11793
11794 frontend.program = program.clone();
11795 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11796 frontend.update_state_after_exec(outcome, true);
11797 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11798 let sketch_id = sketch_object.id;
11799 let sketch = expect_sketch(sketch_object);
11800 let point0_id = *sketch.segments.first().unwrap();
11801 let point1_id = *sketch.segments.get(1).unwrap();
11802 let label_position = Point2d {
11803 x: Number {
11804 value: 10.0,
11805 units: NumericSuffix::Mm,
11806 },
11807 y: Number {
11808 value: 11.0,
11809 units: NumericSuffix::Mm,
11810 },
11811 };
11812
11813 let constraint = Constraint::VerticalDistance(Distance {
11814 points: vec![point0_id.into(), point1_id.into()],
11815 distance: Number {
11816 value: 2.0,
11817 units: NumericSuffix::Mm,
11818 },
11819 label_position: Some(label_position.clone()),
11820 source: Default::default(),
11821 });
11822 let (src_delta, scene_delta) = frontend
11823 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11824 .await
11825 .unwrap();
11826 assert_eq!(
11827 src_delta.text.as_str(),
11828 "\
11830sketch(on = XY) {
11831 point1 = point(at = [var 1, var 2])
11832 point2 = point(at = [var 3, var 4])
11833 verticalDistance([point1, point2], labelPosition = [10mm, 11mm]) == 2mm
11834}
11835"
11836 );
11837 assert_eq!(
11838 scene_delta.new_graph.objects.len(),
11839 5,
11840 "{:#?}",
11841 scene_delta.new_graph.objects
11842 );
11843 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
11844 let sketch = expect_sketch(sketch_object);
11845 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
11846 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11847 panic!("Expected constraint object");
11848 };
11849 let Constraint::VerticalDistance(distance) = constraint else {
11850 panic!("Expected vertical distance constraint");
11851 };
11852 assert_eq!(distance.label_position, Some(label_position));
11853
11854 mock_ctx.close().await;
11855 }
11856
11857 #[tokio::test(flavor = "multi_thread")]
11858 async fn test_add_fixed_standalone_point() {
11859 let initial_source = "\
11860sketch(on = XY) {
11861 point(at = [var 1, var 2])
11862}
11863";
11864
11865 let program = Program::parse(initial_source).unwrap().0.unwrap();
11866
11867 let mut frontend = FrontendState::new();
11868
11869 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11870 let mock_ctx = ExecutorContext::new_mock(None).await;
11871 let version = Version(0);
11872
11873 frontend.hack_set_program(&ctx, program).await.unwrap();
11874 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11875 let sketch_id = sketch_object.id;
11876 let sketch = expect_sketch(sketch_object);
11877 let point_id = *sketch.segments.first().unwrap();
11878
11879 let (src_delta, scene_delta) = frontend
11880 .add_constraint(
11881 &mock_ctx,
11882 version,
11883 sketch_id,
11884 Constraint::Fixed(Fixed {
11885 points: vec![FixedPoint {
11886 point: point_id,
11887 position: Point2d {
11888 x: Number {
11889 value: 2.0,
11890 units: NumericSuffix::Mm,
11891 },
11892 y: Number {
11893 value: 3.0,
11894 units: NumericSuffix::Mm,
11895 },
11896 },
11897 }],
11898 }),
11899 )
11900 .await
11901 .unwrap();
11902 assert_eq!(
11903 src_delta.text.as_str(),
11904 "\
11905sketch(on = XY) {
11906 point1 = point(at = [var 2, var 3])
11907 fixed([point1, [2mm, 3mm]])
11908}
11909"
11910 );
11911 assert_eq!(
11912 scene_delta.new_graph.objects.len(),
11913 4,
11914 "{:#?}",
11915 scene_delta.new_graph.objects
11916 );
11917
11918 ctx.close().await;
11919 mock_ctx.close().await;
11920 }
11921
11922 #[tokio::test(flavor = "multi_thread")]
11923 async fn test_add_fixed_multiple_points() {
11924 let initial_source = "\
11925sketch(on = XY) {
11926 point(at = [var 1, var 2])
11927 point(at = [var 3, var 4])
11928}
11929";
11930
11931 let program = Program::parse(initial_source).unwrap().0.unwrap();
11932
11933 let mut frontend = FrontendState::new();
11934
11935 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11936 let mock_ctx = ExecutorContext::new_mock(None).await;
11937 let version = Version(0);
11938
11939 frontend.hack_set_program(&ctx, program).await.unwrap();
11940 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11941 let sketch_id = sketch_object.id;
11942 let sketch = expect_sketch(sketch_object);
11943 let point0_id = *sketch.segments.first().unwrap();
11944 let point1_id = *sketch.segments.get(1).unwrap();
11945
11946 let (src_delta, scene_delta) = frontend
11947 .add_constraint(
11948 &mock_ctx,
11949 version,
11950 sketch_id,
11951 Constraint::Fixed(Fixed {
11952 points: vec![
11953 FixedPoint {
11954 point: point0_id,
11955 position: Point2d {
11956 x: Number {
11957 value: 2.0,
11958 units: NumericSuffix::Mm,
11959 },
11960 y: Number {
11961 value: 3.0,
11962 units: NumericSuffix::Mm,
11963 },
11964 },
11965 },
11966 FixedPoint {
11967 point: point1_id,
11968 position: Point2d {
11969 x: Number {
11970 value: 4.0,
11971 units: NumericSuffix::Mm,
11972 },
11973 y: Number {
11974 value: 5.0,
11975 units: NumericSuffix::Mm,
11976 },
11977 },
11978 },
11979 ],
11980 }),
11981 )
11982 .await
11983 .unwrap();
11984 assert_eq!(
11985 src_delta.text.as_str(),
11986 "\
11987sketch(on = XY) {
11988 point1 = point(at = [var 2, var 3])
11989 point2 = point(at = [var 4, var 5])
11990 fixed([point1, [2mm, 3mm]])
11991 fixed([point2, [4mm, 5mm]])
11992}
11993"
11994 );
11995 assert_eq!(
11996 scene_delta.new_graph.objects.len(),
11997 6,
11998 "{:#?}",
11999 scene_delta.new_graph.objects
12000 );
12001
12002 ctx.close().await;
12003 mock_ctx.close().await;
12004 }
12005
12006 #[tokio::test(flavor = "multi_thread")]
12007 async fn test_add_fixed_owned_point() {
12008 let initial_source = "\
12009sketch(on = XY) {
12010 line(start = [var 1, var 2], end = [var 3, var 4])
12011}
12012";
12013
12014 let program = Program::parse(initial_source).unwrap().0.unwrap();
12015
12016 let mut frontend = FrontendState::new();
12017
12018 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12019 let mock_ctx = ExecutorContext::new_mock(None).await;
12020 let version = Version(0);
12021
12022 frontend.hack_set_program(&ctx, program).await.unwrap();
12023 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12024 let sketch_id = sketch_object.id;
12025 let sketch = expect_sketch(sketch_object);
12026 let line_start_id = *sketch.segments.first().unwrap();
12027
12028 let (src_delta, scene_delta) = frontend
12029 .add_constraint(
12030 &mock_ctx,
12031 version,
12032 sketch_id,
12033 Constraint::Fixed(Fixed {
12034 points: vec![FixedPoint {
12035 point: line_start_id,
12036 position: Point2d {
12037 x: Number {
12038 value: 2.0,
12039 units: NumericSuffix::Mm,
12040 },
12041 y: Number {
12042 value: 3.0,
12043 units: NumericSuffix::Mm,
12044 },
12045 },
12046 }],
12047 }),
12048 )
12049 .await
12050 .unwrap();
12051 assert_eq!(
12052 src_delta.text.as_str(),
12053 "\
12054sketch(on = XY) {
12055 line1 = line(start = [var 2, var 3], end = [var 3, var 4])
12056 fixed([line1.start, [2mm, 3mm]])
12057}
12058"
12059 );
12060 assert_eq!(
12061 scene_delta.new_graph.objects.len(),
12062 6,
12063 "{:#?}",
12064 scene_delta.new_graph.objects
12065 );
12066
12067 ctx.close().await;
12068 mock_ctx.close().await;
12069 }
12070
12071 #[tokio::test(flavor = "multi_thread")]
12072 async fn test_radius_error_cases() {
12073 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12074 let mock_ctx = ExecutorContext::new_mock(None).await;
12075 let version = Version(0);
12076
12077 let initial_source_point = "\
12079sketch(on = XY) {
12080 point(at = [var 1, var 2])
12081}
12082";
12083 let program_point = Program::parse(initial_source_point).unwrap().0.unwrap();
12084 let mut frontend_point = FrontendState::new();
12085 frontend_point.hack_set_program(&ctx, program_point).await.unwrap();
12086 let sketch_object_point = find_first_sketch_object(&frontend_point.scene_graph).unwrap();
12087 let sketch_id_point = sketch_object_point.id;
12088 let sketch_point = expect_sketch(sketch_object_point);
12089 let point_id = *sketch_point.segments.first().unwrap();
12090
12091 let constraint_point = Constraint::Radius(Radius {
12092 arc: point_id,
12093 radius: Number {
12094 value: 5.0,
12095 units: NumericSuffix::Mm,
12096 },
12097 label_position: None,
12098 source: Default::default(),
12099 });
12100 let result_point = frontend_point
12101 .add_constraint(&mock_ctx, version, sketch_id_point, constraint_point)
12102 .await;
12103 assert!(result_point.is_err(), "Single point should error for radius");
12104
12105 let initial_source_line = "\
12107sketch(on = XY) {
12108 line(start = [var 1, var 2], end = [var 3, var 4])
12109}
12110";
12111 let program_line = Program::parse(initial_source_line).unwrap().0.unwrap();
12112 let mut frontend_line = FrontendState::new();
12113 frontend_line.hack_set_program(&ctx, program_line).await.unwrap();
12114 let sketch_object_line = find_first_sketch_object(&frontend_line.scene_graph).unwrap();
12115 let sketch_id_line = sketch_object_line.id;
12116 let sketch_line = expect_sketch(sketch_object_line);
12117 let line_id = *sketch_line.segments.first().unwrap();
12118
12119 let constraint_line = Constraint::Radius(Radius {
12120 arc: line_id,
12121 radius: Number {
12122 value: 5.0,
12123 units: NumericSuffix::Mm,
12124 },
12125 label_position: None,
12126 source: Default::default(),
12127 });
12128 let result_line = frontend_line
12129 .add_constraint(&mock_ctx, version, sketch_id_line, constraint_line)
12130 .await;
12131 assert!(result_line.is_err(), "Single line segment should error for radius");
12132
12133 ctx.close().await;
12134 mock_ctx.close().await;
12135 }
12136
12137 #[tokio::test(flavor = "multi_thread")]
12138 async fn test_diameter_single_arc_segment() {
12139 let initial_source = "\
12140sketch(on = XY) {
12141 arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
12142}
12143";
12144
12145 let program = Program::parse(initial_source).unwrap().0.unwrap();
12146
12147 let mut frontend = FrontendState::new();
12148
12149 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12150 let mock_ctx = ExecutorContext::new_mock(None).await;
12151 let version = Version(0);
12152
12153 frontend.hack_set_program(&ctx, program).await.unwrap();
12154 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12155 let sketch_id = sketch_object.id;
12156 let sketch = expect_sketch(sketch_object);
12157 let arc_id = sketch
12159 .segments
12160 .iter()
12161 .find(|&seg_id| {
12162 let obj = frontend.scene_graph.objects.get(seg_id.0);
12163 matches!(
12164 obj.map(|o| &o.kind),
12165 Some(ObjectKind::Segment {
12166 segment: Segment::Arc(_)
12167 })
12168 )
12169 })
12170 .unwrap();
12171
12172 let constraint = Constraint::Diameter(Diameter {
12173 arc: *arc_id,
12174 diameter: Number {
12175 value: 10.0,
12176 units: NumericSuffix::Mm,
12177 },
12178 label_position: None,
12179 source: Default::default(),
12180 });
12181 let (src_delta, scene_delta) = frontend
12182 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12183 .await
12184 .unwrap();
12185 assert_eq!(
12186 src_delta.text.as_str(),
12187 "\
12189sketch(on = XY) {
12190 arc1 = arc(start = [var 1.83, var 3.62], end = [var 2.42, var 3.3], center = [var -0.25, var -0.92])
12191 diameter(arc1) == 10mm
12192}
12193"
12194 );
12195 assert_eq!(
12196 scene_delta.new_graph.objects.len(),
12197 7, "{:#?}",
12199 scene_delta.new_graph.objects
12200 );
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_with_label_position() {
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 let mut frontend = FrontendState::new();
12216 let mock_ctx = ExecutorContext::new_mock(None).await;
12217 let version = Version(0);
12218
12219 frontend.program = program.clone();
12220 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12221 frontend.update_state_after_exec(outcome, true);
12222 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12223 let sketch_id = sketch_object.id;
12224 let sketch = expect_sketch(sketch_object);
12225 let arc_id = sketch
12226 .segments
12227 .iter()
12228 .find(|&seg_id| {
12229 let obj = frontend.scene_graph.objects.get(seg_id.0);
12230 matches!(
12231 obj.map(|o| &o.kind),
12232 Some(ObjectKind::Segment {
12233 segment: Segment::Arc(_)
12234 })
12235 )
12236 })
12237 .unwrap();
12238
12239 let label_position = Point2d {
12240 x: Number {
12241 value: 10.0,
12242 units: NumericSuffix::Mm,
12243 },
12244 y: Number {
12245 value: 11.0,
12246 units: NumericSuffix::Mm,
12247 },
12248 };
12249 let constraint = Constraint::Diameter(Diameter {
12250 arc: *arc_id,
12251 diameter: Number {
12252 value: 10.0,
12253 units: NumericSuffix::Mm,
12254 },
12255 label_position: Some(label_position.clone()),
12256 source: Default::default(),
12257 });
12258 let (src_delta, scene_delta) = frontend
12259 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12260 .await
12261 .unwrap();
12262 assert_eq!(
12263 src_delta.text.as_str(),
12264 "\
12265sketch(on = XY) {
12266 arc1 = arc(start = [var 1.83, var 3.62], end = [var 2.42, var 3.3], center = [var -0.25, var -0.92])
12267 diameter(arc1, labelPosition = [10mm, 11mm]) == 10mm
12268}
12269"
12270 );
12271
12272 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
12273 let sketch = expect_sketch(sketch_object);
12274 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
12275 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12276 panic!("Expected constraint object");
12277 };
12278 let Constraint::Diameter(diameter) = constraint else {
12279 panic!("Expected diameter constraint");
12280 };
12281 assert_eq!(diameter.label_position, Some(label_position));
12282
12283 mock_ctx.close().await;
12284 }
12285
12286 #[tokio::test(flavor = "multi_thread")]
12287 async fn test_edit_diameter_constraint_label_position() {
12288 let initial_source = "\
12289sketch(on = XY) {
12290 arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
12291 diameter(arc1) == 10mm
12292}
12293";
12294
12295 let program = Program::parse(initial_source).unwrap().0.unwrap();
12296 let mut frontend = FrontendState::new();
12297 let mock_ctx = ExecutorContext::new_mock(None).await;
12298 let version = Version(0);
12299
12300 frontend.program = program.clone();
12301 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12302 frontend.update_state_after_exec(outcome, true);
12303 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12304 let sketch_id = sketch_object.id;
12305 let sketch = expect_sketch(sketch_object);
12306 let constraint_id = sketch.constraints[0];
12307 let label_position = Point2d {
12308 x: Number {
12309 value: 10.0,
12310 units: NumericSuffix::Mm,
12311 },
12312 y: Number {
12313 value: 11.0,
12314 units: NumericSuffix::Mm,
12315 },
12316 };
12317
12318 let (src_delta, scene_delta) = frontend
12319 .edit_distance_constraint_label_position(
12320 &mock_ctx,
12321 version,
12322 sketch_id,
12323 constraint_id,
12324 label_position.clone(),
12325 vec![],
12326 )
12327 .await
12328 .unwrap();
12329 assert_eq!(
12330 src_delta.text.as_str(),
12331 "\
12332sketch(on = XY) {
12333 arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
12334 diameter(arc1, labelPosition = [10mm, 11mm]) == 10mm
12335}
12336"
12337 );
12338
12339 let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
12340 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12341 panic!("Expected constraint object");
12342 };
12343 let Constraint::Diameter(diameter) = constraint else {
12344 panic!("Expected diameter constraint");
12345 };
12346 assert_eq!(diameter.label_position, Some(label_position));
12347
12348 mock_ctx.close().await;
12349 }
12350
12351 #[tokio::test(flavor = "multi_thread")]
12352 async fn test_diameter_error_cases() {
12353 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12354 let mock_ctx = ExecutorContext::new_mock(None).await;
12355 let version = Version(0);
12356
12357 let initial_source_point = "\
12359sketch(on = XY) {
12360 point(at = [var 1, var 2])
12361}
12362";
12363 let program_point = Program::parse(initial_source_point).unwrap().0.unwrap();
12364 let mut frontend_point = FrontendState::new();
12365 frontend_point.hack_set_program(&ctx, program_point).await.unwrap();
12366 let sketch_object_point = find_first_sketch_object(&frontend_point.scene_graph).unwrap();
12367 let sketch_id_point = sketch_object_point.id;
12368 let sketch_point = expect_sketch(sketch_object_point);
12369 let point_id = *sketch_point.segments.first().unwrap();
12370
12371 let constraint_point = Constraint::Diameter(Diameter {
12372 arc: point_id,
12373 diameter: Number {
12374 value: 10.0,
12375 units: NumericSuffix::Mm,
12376 },
12377 label_position: None,
12378 source: Default::default(),
12379 });
12380 let result_point = frontend_point
12381 .add_constraint(&mock_ctx, version, sketch_id_point, constraint_point)
12382 .await;
12383 assert!(result_point.is_err(), "Single point should error for diameter");
12384
12385 let initial_source_line = "\
12387sketch(on = XY) {
12388 line(start = [var 1, var 2], end = [var 3, var 4])
12389}
12390";
12391 let program_line = Program::parse(initial_source_line).unwrap().0.unwrap();
12392 let mut frontend_line = FrontendState::new();
12393 frontend_line.hack_set_program(&ctx, program_line).await.unwrap();
12394 let sketch_object_line = find_first_sketch_object(&frontend_line.scene_graph).unwrap();
12395 let sketch_id_line = sketch_object_line.id;
12396 let sketch_line = expect_sketch(sketch_object_line);
12397 let line_id = *sketch_line.segments.first().unwrap();
12398
12399 let constraint_line = Constraint::Diameter(Diameter {
12400 arc: line_id,
12401 diameter: Number {
12402 value: 10.0,
12403 units: NumericSuffix::Mm,
12404 },
12405 label_position: None,
12406 source: Default::default(),
12407 });
12408 let result_line = frontend_line
12409 .add_constraint(&mock_ctx, version, sketch_id_line, constraint_line)
12410 .await;
12411 assert!(result_line.is_err(), "Single line segment should error for diameter");
12412
12413 ctx.close().await;
12414 mock_ctx.close().await;
12415 }
12416
12417 #[tokio::test(flavor = "multi_thread")]
12418 async fn test_line_horizontal() {
12419 let initial_source = "\
12420sketch(on = XY) {
12421 line(start = [var 1, var 2], end = [var 3, var 4])
12422}
12423";
12424
12425 let program = Program::parse(initial_source).unwrap().0.unwrap();
12426
12427 let mut frontend = FrontendState::new();
12428
12429 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12430 let mock_ctx = ExecutorContext::new_mock(None).await;
12431 let version = Version(0);
12432
12433 frontend.hack_set_program(&ctx, program).await.unwrap();
12434 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12435 let sketch_id = sketch_object.id;
12436 let sketch = expect_sketch(sketch_object);
12437 let line1_id = *sketch.segments.get(2).unwrap();
12438
12439 let constraint = Constraint::Horizontal(Horizontal::Line { line: line1_id });
12440 let (src_delta, scene_delta) = frontend
12441 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12442 .await
12443 .unwrap();
12444 assert_eq!(
12445 src_delta.text.as_str(),
12446 "\
12447sketch(on = XY) {
12448 line1 = line(start = [var 1, var 3], end = [var 3, var 3])
12449 horizontal(line1)
12450}
12451"
12452 );
12453 assert_eq!(
12454 scene_delta.new_graph.objects.len(),
12455 6,
12456 "{:#?}",
12457 scene_delta.new_graph.objects
12458 );
12459
12460 ctx.close().await;
12461 mock_ctx.close().await;
12462 }
12463
12464 #[tokio::test(flavor = "multi_thread")]
12465 async fn test_control_point_spline_edge_horizontal() {
12466 let initial_source = "\
12467@settings(experimentalFeatures = allow)
12468splineSketch = sketch(on = XY) {
12469 controlPointSpline1 = controlPointSpline(points = [
12470 [var 0mm, var 0mm],
12471 [var 10mm, var 20mm],
12472 [var 20mm, var 0mm],
12473 ])
12474}
12475";
12476
12477 let program = Program::parse(initial_source).unwrap().0.unwrap();
12478
12479 let mut frontend = FrontendState::new();
12480
12481 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12482 let mock_ctx = ExecutorContext::new_mock(None).await;
12483 let version = Version(0);
12484
12485 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
12486 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12487 let sketch_id = sketch_object.id;
12488 let sketch = expect_sketch(sketch_object);
12489 let spline_id = sketch
12490 .segments
12491 .iter()
12492 .copied()
12493 .find(|seg_id| {
12494 matches!(
12495 &frontend.scene_graph.objects[seg_id.0].kind,
12496 ObjectKind::Segment {
12497 segment: Segment::ControlPointSpline(_)
12498 }
12499 )
12500 })
12501 .expect("Expected a control point spline segment in sketch");
12502 let edge_id = frontend
12503 .scene_graph
12504 .objects
12505 .iter()
12506 .find_map(|obj| match &obj.kind {
12507 ObjectKind::Segment {
12508 segment: Segment::Line(line),
12509 } if line.owner == Some(spline_id) => Some(obj.id),
12510 _ => None,
12511 })
12512 .expect("Expected an owned control-polygon edge");
12513
12514 let constraint = Constraint::Horizontal(Horizontal::Line { line: edge_id });
12515 let (src_delta, _) = frontend
12516 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12517 .await
12518 .unwrap();
12519 assert!(
12520 src_delta.text.contains("horizontal(controlPointSpline1.edges[0])"),
12521 "Expected horizontal constraint on spline edge, got: {}",
12522 src_delta.text
12523 );
12524
12525 ctx.close().await;
12526 mock_ctx.close().await;
12527 }
12528
12529 #[tokio::test(flavor = "multi_thread")]
12530 async fn test_control_point_spline_edge_angle() {
12531 let initial_source = "\
12532@settings(experimentalFeatures = allow)
12533splineSketch = sketch(on = XY) {
12534 controlPointSpline1 = controlPointSpline(points = [
12535 [var 0mm, var 0mm],
12536 [var 10mm, var 20mm],
12537 [var 20mm, var 0mm],
12538 ])
12539
12540 line1 = line(start = [var 40mm, var 0mm], end = [var 60mm, var 10mm])
12541}
12542";
12543
12544 let program = Program::parse(initial_source).unwrap().0.unwrap();
12545
12546 let mut frontend = FrontendState::new();
12547
12548 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12549 let mock_ctx = ExecutorContext::new_mock(None).await;
12550 let version = Version(0);
12551
12552 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
12553 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12554 let sketch_id = sketch_object.id;
12555 let sketch = expect_sketch(sketch_object);
12556 let spline_id = sketch
12557 .segments
12558 .iter()
12559 .copied()
12560 .find(|seg_id| {
12561 matches!(
12562 &frontend.scene_graph.objects[seg_id.0].kind,
12563 ObjectKind::Segment {
12564 segment: Segment::ControlPointSpline(_)
12565 }
12566 )
12567 })
12568 .expect("Expected a control point spline segment in sketch");
12569 let edge_id = frontend
12570 .scene_graph
12571 .objects
12572 .iter()
12573 .find_map(|obj| match &obj.kind {
12574 ObjectKind::Segment {
12575 segment: Segment::Line(line),
12576 } if line.owner == Some(spline_id) => Some(obj.id),
12577 _ => None,
12578 })
12579 .expect("Expected an owned control-polygon edge");
12580 let line1_id = frontend
12581 .scene_graph
12582 .objects
12583 .iter()
12584 .find_map(|obj| match &obj.kind {
12585 ObjectKind::Segment {
12586 segment: Segment::Line(line),
12587 } if line.owner.is_none() && obj.label == "line1" => Some(obj.id),
12588 _ => None,
12589 })
12590 .or_else(|| {
12591 sketch.segments.iter().copied().find(|seg_id| {
12592 matches!(
12593 &frontend.scene_graph.objects[seg_id.0].kind,
12594 ObjectKind::Segment {
12595 segment: Segment::Line(line),
12596 } if line.owner.is_none()
12597 )
12598 })
12599 })
12600 .expect("Expected a standalone line segment in sketch");
12601
12602 let constraint = Constraint::Angle(Angle {
12603 lines: vec![line1_id, edge_id],
12604 angle: Number {
12605 value: 30.0,
12606 units: NumericSuffix::Deg,
12607 },
12608 source: Default::default(),
12609 });
12610 let (src_delta, _) = frontend
12611 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12612 .await
12613 .unwrap();
12614 assert!(
12615 src_delta
12616 .text
12617 .contains("angle([line1, controlPointSpline1.edges[0]]) == 30deg"),
12618 "Expected angle constraint on spline edge, got: {}",
12619 src_delta.text
12620 );
12621
12622 ctx.close().await;
12623 mock_ctx.close().await;
12624 }
12625
12626 #[tokio::test(flavor = "multi_thread")]
12627 async fn test_ui_scene_graph_hides_same_spline_coincident_constraints() {
12628 let initial_source = "\
12629@settings(experimentalFeatures = allow)
12630splineSketch = sketch(on = XY) {
12631 spline1 = controlPointSpline(points = [
12632 [var 0mm, var 0mm],
12633 [var 10mm, var 20mm],
12634 [var 20mm, var 0mm],
12635 ])
12636 line1 = line(start = [var 0mm, var 0mm], end = [var -10mm, var 0mm])
12637 coincident([spline1.controls[1], spline1.edges[0]])
12638 coincident([spline1.controls[0], line1])
12639}
12640";
12641
12642 let program = Program::parse(initial_source).unwrap().0.unwrap();
12643
12644 let mut frontend = FrontendState::new();
12645
12646 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12647 let mock_ctx = ExecutorContext::new_mock(None).await;
12648
12649 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
12650
12651 let ui_scene_graph = frontend.scene_graph_for_ui();
12652 let sketch_object = find_first_sketch_object(&ui_scene_graph).unwrap();
12653 let sketch = expect_sketch(sketch_object);
12654
12655 assert_eq!(
12656 sketch.constraints.len(),
12657 1,
12658 "Expected only the external coincident constraint to remain visible in the UI scene graph"
12659 );
12660
12661 let visible_constraints = ui_scene_graph
12662 .objects
12663 .iter()
12664 .filter_map(|object| match &object.kind {
12665 ObjectKind::Constraint {
12666 constraint: Constraint::Coincident(coincident),
12667 } => Some(coincident.clone()),
12668 _ => None,
12669 })
12670 .collect::<Vec<_>>();
12671
12672 assert_eq!(
12673 visible_constraints.len(),
12674 1,
12675 "Expected only one coincident constraint object in the UI scene graph"
12676 );
12677 assert_eq!(
12678 visible_constraints[0].get_segments().len(),
12679 2,
12680 "Expected the remaining visible coincident constraint to reference two segments"
12681 );
12682
12683 ctx.close().await;
12684 mock_ctx.close().await;
12685 }
12686
12687 #[tokio::test(flavor = "multi_thread")]
12688 async fn test_edit_control_point_spline_can_append_control_point() {
12689 let initial_source = "\
12690@settings(experimentalFeatures = allow)
12691splineSketch = sketch(on = XY) {
12692 controlPointSpline(points = [
12693 [var 0mm, var 0mm],
12694 [var 10mm, var 20mm],
12695 [var 20mm, var 0mm],
12696 ])
12697}
12698";
12699
12700 let program = Program::parse(initial_source).unwrap().0.unwrap();
12701
12702 let mut frontend = FrontendState::new();
12703
12704 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12705 let mock_ctx = ExecutorContext::new_mock(None).await;
12706 let version = Version(0);
12707
12708 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
12709 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12710 let sketch_id = sketch_object.id;
12711 let sketch = expect_sketch(sketch_object);
12712 let spline_id = sketch
12713 .segments
12714 .iter()
12715 .copied()
12716 .find(|seg_id| {
12717 matches!(
12718 &frontend.scene_graph.objects[seg_id.0].kind,
12719 ObjectKind::Segment {
12720 segment: Segment::ControlPointSpline(_)
12721 }
12722 )
12723 })
12724 .expect("Expected a control point spline segment in sketch");
12725
12726 let ctor = ControlPointSplineCtor {
12727 points: vec![
12728 Point2d {
12729 x: Expr::Var(Number {
12730 value: 0.0,
12731 units: NumericSuffix::Mm,
12732 }),
12733 y: Expr::Var(Number {
12734 value: 0.0,
12735 units: NumericSuffix::Mm,
12736 }),
12737 },
12738 Point2d {
12739 x: Expr::Var(Number {
12740 value: 10.0,
12741 units: NumericSuffix::Mm,
12742 }),
12743 y: Expr::Var(Number {
12744 value: 20.0,
12745 units: NumericSuffix::Mm,
12746 }),
12747 },
12748 Point2d {
12749 x: Expr::Var(Number {
12750 value: 20.0,
12751 units: NumericSuffix::Mm,
12752 }),
12753 y: Expr::Var(Number {
12754 value: 0.0,
12755 units: NumericSuffix::Mm,
12756 }),
12757 },
12758 Point2d {
12759 x: Expr::Var(Number {
12760 value: 30.0,
12761 units: NumericSuffix::Mm,
12762 }),
12763 y: Expr::Var(Number {
12764 value: 10.0,
12765 units: NumericSuffix::Mm,
12766 }),
12767 },
12768 ],
12769 construction: None,
12770 };
12771
12772 let segments = vec![ExistingSegmentCtor {
12773 id: spline_id,
12774 ctor: SegmentCtor::ControlPointSpline(ctor),
12775 }];
12776 let (src_delta, scene_delta) = frontend
12777 .edit_segments(&mock_ctx, version, sketch_id, segments)
12778 .await
12779 .unwrap();
12780
12781 assert!(
12782 src_delta.text.contains("[var 30mm, var 10mm]"),
12783 "Expected appended spline control point in source, got: {}",
12784 src_delta.text
12785 );
12786
12787 assert!(
12788 scene_delta.invalidates_ids,
12789 "Expected appending a spline control point to invalidate ids"
12790 );
12791 let updated_spline = scene_delta
12792 .new_graph
12793 .objects
12794 .iter()
12795 .find_map(|obj| match &obj.kind {
12796 ObjectKind::Segment {
12797 segment: Segment::ControlPointSpline(updated_spline),
12798 } if updated_spline.controls.len() == 4 => Some(updated_spline),
12799 _ => None,
12800 })
12801 .expect("Expected edited scene graph to contain a four-point control point spline");
12802 assert_eq!(
12803 updated_spline.controls.len(),
12804 4,
12805 "Expected edited spline to expose four control points"
12806 );
12807
12808 ctx.close().await;
12809 mock_ctx.close().await;
12810 }
12811
12812 #[tokio::test(flavor = "multi_thread")]
12813 async fn test_line_vertical() {
12814 let initial_source = "\
12815sketch(on = XY) {
12816 line(start = [var 1, var 2], end = [var 3, var 4])
12817}
12818";
12819
12820 let program = Program::parse(initial_source).unwrap().0.unwrap();
12821
12822 let mut frontend = FrontendState::new();
12823
12824 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12825 let mock_ctx = ExecutorContext::new_mock(None).await;
12826 let version = Version(0);
12827
12828 frontend.hack_set_program(&ctx, program).await.unwrap();
12829 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12830 let sketch_id = sketch_object.id;
12831 let sketch = expect_sketch(sketch_object);
12832 let line1_id = *sketch.segments.get(2).unwrap();
12833
12834 let constraint = Constraint::Vertical(Vertical::Line { line: line1_id });
12835 let (src_delta, scene_delta) = frontend
12836 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12837 .await
12838 .unwrap();
12839 assert_eq!(
12840 src_delta.text.as_str(),
12841 "\
12842sketch(on = XY) {
12843 line1 = line(start = [var 2, var 2], end = [var 2, var 4])
12844 vertical(line1)
12845}
12846"
12847 );
12848 assert_eq!(
12849 scene_delta.new_graph.objects.len(),
12850 6,
12851 "{:#?}",
12852 scene_delta.new_graph.objects
12853 );
12854
12855 ctx.close().await;
12856 mock_ctx.close().await;
12857 }
12858
12859 #[tokio::test(flavor = "multi_thread")]
12860 async fn test_points_vertical() {
12861 let initial_source = "\
12862sketch001 = sketch(on = XY) {
12863 p0 = point(at = [var -2.23mm, var 3.1mm])
12864 pf = point(at = [4, 4])
12865}
12866";
12867
12868 let program = Program::parse(initial_source).unwrap().0.unwrap();
12869
12870 let mut frontend = FrontendState::new();
12871
12872 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12873 let mock_ctx = ExecutorContext::new_mock(None).await;
12874 let version = Version(0);
12875
12876 frontend.hack_set_program(&ctx, program).await.unwrap();
12877 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12878 let sketch_id = sketch_object.id;
12879 let sketch = expect_sketch(sketch_object);
12880 let point_ids = vec![
12881 sketch.segments.first().unwrap().to_owned(),
12882 sketch.segments.get(1).unwrap().to_owned(),
12883 ];
12884
12885 let constraint = Constraint::Vertical(Vertical::Points {
12886 points: point_ids.into_iter().map(ConstraintSegment::from).collect(),
12887 });
12888 let (src_delta, scene_delta) = frontend
12889 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12890 .await
12891 .unwrap();
12892 assert_eq!(
12893 src_delta.text.as_str(),
12894 "\
12895sketch001 = sketch(on = XY) {
12896 p0 = point(at = [var 4mm, var 3.1mm])
12897 pf = point(at = [4, 4])
12898 vertical([p0, pf])
12899}
12900"
12901 );
12902 assert_eq!(
12903 scene_delta.new_graph.objects.len(),
12904 5,
12905 "{:#?}",
12906 scene_delta.new_graph.objects
12907 );
12908
12909 ctx.close().await;
12910 mock_ctx.close().await;
12911 }
12912
12913 #[tokio::test(flavor = "multi_thread")]
12914 async fn test_points_horizontal() {
12915 let initial_source = "\
12916sketch001 = sketch(on = XY) {
12917 p0 = point(at = [var -2.23mm, var 3.1mm])
12918 pf = point(at = [4, 4])
12919}
12920";
12921
12922 let program = Program::parse(initial_source).unwrap().0.unwrap();
12923
12924 let mut frontend = FrontendState::new();
12925
12926 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12927 let mock_ctx = ExecutorContext::new_mock(None).await;
12928 let version = Version(0);
12929
12930 frontend.hack_set_program(&ctx, program).await.unwrap();
12931 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12932 let sketch_id = sketch_object.id;
12933 let sketch = expect_sketch(sketch_object);
12934 let point_ids = vec![
12935 sketch.segments.first().unwrap().to_owned(),
12936 sketch.segments.get(1).unwrap().to_owned(),
12937 ];
12938
12939 let constraint = Constraint::Horizontal(Horizontal::Points {
12940 points: point_ids.into_iter().map(ConstraintSegment::from).collect(),
12941 });
12942 let (src_delta, scene_delta) = frontend
12943 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12944 .await
12945 .unwrap();
12946 assert_eq!(
12947 src_delta.text.as_str(),
12948 "\
12949sketch001 = sketch(on = XY) {
12950 p0 = point(at = [var -2.23mm, var 4mm])
12951 pf = point(at = [4, 4])
12952 horizontal([p0, pf])
12953}
12954"
12955 );
12956 assert_eq!(
12957 scene_delta.new_graph.objects.len(),
12958 5,
12959 "{:#?}",
12960 scene_delta.new_graph.objects
12961 );
12962
12963 ctx.close().await;
12964 mock_ctx.close().await;
12965 }
12966
12967 #[tokio::test(flavor = "multi_thread")]
12968 async fn test_point_horizontal_with_origin() {
12969 let initial_source = "\
12970sketch001 = sketch(on = XY) {
12971 p0 = point(at = [var -2.23mm, var 3.1mm])
12972}
12973";
12974
12975 let program = Program::parse(initial_source).unwrap().0.unwrap();
12976
12977 let mut frontend = FrontendState::new();
12978
12979 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12980 let mock_ctx = ExecutorContext::new_mock(None).await;
12981 let version = Version(0);
12982
12983 frontend.hack_set_program(&ctx, program).await.unwrap();
12984 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12985 let sketch_id = sketch_object.id;
12986 let sketch = expect_sketch(sketch_object);
12987 let point_id = *sketch.segments.first().unwrap();
12988
12989 let constraint = Constraint::Horizontal(Horizontal::Points {
12990 points: vec![ConstraintSegment::from(point_id), ConstraintSegment::ORIGIN],
12991 });
12992 let (src_delta, scene_delta) = frontend
12993 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12994 .await
12995 .unwrap();
12996 assert_eq!(
12997 src_delta.text.as_str(),
12998 "\
12999sketch001 = sketch(on = XY) {
13000 p0 = point(at = [var -2.23mm, var 0mm])
13001 horizontal([p0, ORIGIN])
13002}
13003"
13004 );
13005 assert_eq!(
13006 scene_delta.new_graph.objects.len(),
13007 4,
13008 "{:#?}",
13009 scene_delta.new_graph.objects
13010 );
13011
13012 ctx.close().await;
13013 mock_ctx.close().await;
13014 }
13015
13016 #[tokio::test(flavor = "multi_thread")]
13017 async fn test_lines_equal_length() {
13018 let initial_source = "\
13019sketch(on = XY) {
13020 line(start = [var 1, var 2], end = [var 3, var 4])
13021 line(start = [var 5, var 6], end = [var 7, var 8])
13022}
13023";
13024
13025 let program = Program::parse(initial_source).unwrap().0.unwrap();
13026
13027 let mut frontend = FrontendState::new();
13028
13029 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13030 let mock_ctx = ExecutorContext::new_mock(None).await;
13031 let version = Version(0);
13032
13033 frontend.hack_set_program(&ctx, program).await.unwrap();
13034 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13035 let sketch_id = sketch_object.id;
13036 let sketch = expect_sketch(sketch_object);
13037 let line1_id = *sketch.segments.get(2).unwrap();
13038 let line2_id = *sketch.segments.get(5).unwrap();
13039
13040 let constraint = Constraint::LinesEqualLength(LinesEqualLength {
13041 lines: vec![line1_id, line2_id],
13042 });
13043 let (src_delta, scene_delta) = frontend
13044 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13045 .await
13046 .unwrap();
13047 assert_eq!(
13048 src_delta.text.as_str(),
13049 "\
13050sketch(on = XY) {
13051 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
13052 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
13053 equalLength([line1, line2])
13054}
13055"
13056 );
13057 assert_eq!(
13058 scene_delta.new_graph.objects.len(),
13059 9,
13060 "{:#?}",
13061 scene_delta.new_graph.objects
13062 );
13063
13064 ctx.close().await;
13065 mock_ctx.close().await;
13066 }
13067
13068 #[tokio::test(flavor = "multi_thread")]
13069 async fn test_add_constraint_multi_line_equal_length() {
13070 let initial_source = "\
13071sketch(on = XY) {
13072 line(start = [var 1, var 2], end = [var 3, var 4])
13073 line(start = [var 5, var 6], end = [var 7, var 8])
13074 line(start = [var 9, var 10], end = [var 11, var 12])
13075}
13076";
13077
13078 let program = Program::parse(initial_source).unwrap().0.unwrap();
13079
13080 let mut frontend = FrontendState::new();
13081 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13082 let mock_ctx = ExecutorContext::new_mock(None).await;
13083 let version = Version(0);
13084
13085 frontend.hack_set_program(&ctx, program).await.unwrap();
13086 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13087 let sketch_id = sketch_object.id;
13088 let sketch = expect_sketch(sketch_object);
13089 let line1_id = *sketch.segments.get(2).unwrap();
13090 let line2_id = *sketch.segments.get(5).unwrap();
13091 let line3_id = *sketch.segments.get(8).unwrap();
13092
13093 let constraint = Constraint::LinesEqualLength(LinesEqualLength {
13094 lines: vec![line1_id, line2_id, line3_id],
13095 });
13096 let (src_delta, scene_delta) = frontend
13097 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13098 .await
13099 .unwrap();
13100 assert_eq!(
13101 src_delta.text.as_str(),
13102 "\
13103sketch(on = XY) {
13104 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
13105 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
13106 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
13107 equalLength([line1, line2, line3])
13108}
13109"
13110 );
13111 let constraints = scene_delta
13112 .new_graph
13113 .objects
13114 .iter()
13115 .filter_map(|obj| {
13116 let ObjectKind::Constraint { constraint } = &obj.kind else {
13117 return None;
13118 };
13119 Some(constraint)
13120 })
13121 .collect::<Vec<_>>();
13122
13123 assert_eq!(constraints.len(), 1, "{:#?}", frontend.scene_graph.objects);
13124 let Constraint::LinesEqualLength(lines_equal_length) = constraints[0] else {
13125 panic!("expected equal length constraint, got {:?}", constraints[0]);
13126 };
13127 assert_eq!(lines_equal_length.lines.len(), 3);
13128
13129 ctx.close().await;
13130 mock_ctx.close().await;
13131 }
13132
13133 #[tokio::test(flavor = "multi_thread")]
13134 async fn test_lines_parallel() {
13135 let initial_source = "\
13136sketch(on = XY) {
13137 line(start = [var 1, var 2], end = [var 3, var 4])
13138 line(start = [var 5, var 6], end = [var 7, var 8])
13139}
13140";
13141
13142 let program = Program::parse(initial_source).unwrap().0.unwrap();
13143
13144 let mut frontend = FrontendState::new();
13145
13146 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13147 let mock_ctx = ExecutorContext::new_mock(None).await;
13148 let version = Version(0);
13149
13150 frontend.hack_set_program(&ctx, program).await.unwrap();
13151 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13152 let sketch_id = sketch_object.id;
13153 let sketch = expect_sketch(sketch_object);
13154 let line1_id = *sketch.segments.get(2).unwrap();
13155 let line2_id = *sketch.segments.get(5).unwrap();
13156
13157 let constraint = Constraint::Parallel(Parallel {
13158 lines: vec![line1_id, line2_id],
13159 });
13160 let (src_delta, scene_delta) = frontend
13161 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13162 .await
13163 .unwrap();
13164 assert_eq!(
13165 src_delta.text.as_str(),
13166 "\
13167sketch(on = XY) {
13168 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
13169 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
13170 parallel([line1, line2])
13171}
13172"
13173 );
13174 assert_eq!(
13175 scene_delta.new_graph.objects.len(),
13176 9,
13177 "{:#?}",
13178 scene_delta.new_graph.objects
13179 );
13180
13181 ctx.close().await;
13182 mock_ctx.close().await;
13183 }
13184
13185 #[tokio::test(flavor = "multi_thread")]
13186 async fn test_lines_parallel_multiline() {
13187 let initial_source = "\
13188sketch(on = XY) {
13189 line(start = [var 1, var 2], end = [var 3, var 4])
13190 line(start = [var 5, var 6], end = [var 7, var 8])
13191 line(start = [var 9, var 10], end = [var 11, var 12])
13192}
13193";
13194
13195 let program = Program::parse(initial_source).unwrap().0.unwrap();
13196
13197 let mut frontend = FrontendState::new();
13198
13199 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13200 let mock_ctx = ExecutorContext::new_mock(None).await;
13201 let version = Version(0);
13202
13203 frontend.hack_set_program(&ctx, program).await.unwrap();
13204 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13205 let sketch_id = sketch_object.id;
13206 let sketch = expect_sketch(sketch_object);
13207 let line1_id = *sketch.segments.get(2).unwrap();
13208 let line2_id = *sketch.segments.get(5).unwrap();
13209 let line3_id = *sketch.segments.get(8).unwrap();
13210
13211 let constraint = Constraint::Parallel(Parallel {
13212 lines: vec![line1_id, line2_id, line3_id],
13213 });
13214 let (src_delta, scene_delta) = frontend
13215 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13216 .await
13217 .unwrap();
13218 assert_eq!(
13219 src_delta.text.as_str(),
13220 "\
13221sketch(on = XY) {
13222 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
13223 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
13224 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
13225 parallel([line1, line2, line3])
13226}
13227"
13228 );
13229
13230 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
13231 let sketch = expect_sketch(sketch_object);
13232 assert_eq!(sketch.constraints.len(), 1);
13233
13234 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
13235 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
13236 panic!("Expected constraint object");
13237 };
13238 let Constraint::Parallel(parallel) = constraint else {
13239 panic!("Expected parallel constraint");
13240 };
13241 assert_eq!(parallel.lines.len(), 3);
13242
13243 ctx.close().await;
13244 mock_ctx.close().await;
13245 }
13246
13247 #[tokio::test(flavor = "multi_thread")]
13248 async fn test_lines_perpendicular() {
13249 let initial_source = "\
13250sketch(on = XY) {
13251 line(start = [var 1, var 2], end = [var 3, var 4])
13252 line(start = [var 5, var 6], end = [var 7, var 8])
13253}
13254";
13255
13256 let program = Program::parse(initial_source).unwrap().0.unwrap();
13257
13258 let mut frontend = FrontendState::new();
13259
13260 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13261 let mock_ctx = ExecutorContext::new_mock(None).await;
13262 let version = Version(0);
13263
13264 frontend.hack_set_program(&ctx, program).await.unwrap();
13265 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13266 let sketch_id = sketch_object.id;
13267 let sketch = expect_sketch(sketch_object);
13268 let line1_id = *sketch.segments.get(2).unwrap();
13269 let line2_id = *sketch.segments.get(5).unwrap();
13270
13271 let constraint = Constraint::Perpendicular(Perpendicular {
13272 lines: vec![line1_id, line2_id],
13273 });
13274 let (src_delta, scene_delta) = frontend
13275 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13276 .await
13277 .unwrap();
13278 assert_eq!(
13279 src_delta.text.as_str(),
13280 "\
13281sketch(on = XY) {
13282 line1 = line(start = [var 2, var 3], end = [var 2, var 3])
13283 line2 = line(start = [var 6, var 7], end = [var 6, var 7])
13284 perpendicular([line1, line2])
13285}
13286"
13287 );
13288 assert_eq!(
13289 scene_delta.new_graph.objects.len(),
13290 9,
13291 "{:#?}",
13292 scene_delta.new_graph.objects
13293 );
13294
13295 ctx.close().await;
13296 mock_ctx.close().await;
13297 }
13298
13299 #[tokio::test(flavor = "multi_thread")]
13300 async fn test_lines_angle() {
13301 let initial_source = "\
13302sketch(on = XY) {
13303 line(start = [var 1, var 2], end = [var 3, var 4])
13304 line(start = [var 5, var 6], end = [var 7, var 8])
13305}
13306";
13307
13308 let program = Program::parse(initial_source).unwrap().0.unwrap();
13309
13310 let mut frontend = FrontendState::new();
13311
13312 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13313 let mock_ctx = ExecutorContext::new_mock(None).await;
13314 let version = Version(0);
13315
13316 frontend.hack_set_program(&ctx, program).await.unwrap();
13317 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13318 let sketch_id = sketch_object.id;
13319 let sketch = expect_sketch(sketch_object);
13320 let line1_id = *sketch.segments.get(2).unwrap();
13321 let line2_id = *sketch.segments.get(5).unwrap();
13322
13323 let constraint = Constraint::Angle(Angle {
13324 lines: vec![line1_id, line2_id],
13325 angle: Number {
13326 value: 30.0,
13327 units: NumericSuffix::Deg,
13328 },
13329 source: Default::default(),
13330 });
13331 let (src_delta, scene_delta) = frontend
13332 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13333 .await
13334 .unwrap();
13335 assert_eq!(
13336 src_delta.text.as_str(),
13337 "\
13339sketch(on = XY) {
13340 line1 = line(start = [var 0.9, var 2.36], end = [var 3.1, var 3.64])
13341 line2 = line(start = [var 5.36, var 5.9], end = [var 6.64, var 8.1])
13342 angle([line1, line2]) == 30deg
13343}
13344"
13345 );
13346 assert_eq!(
13347 scene_delta.new_graph.objects.len(),
13348 9,
13349 "{:#?}",
13350 scene_delta.new_graph.objects
13351 );
13352
13353 ctx.close().await;
13354 mock_ctx.close().await;
13355 }
13356
13357 #[tokio::test(flavor = "multi_thread")]
13358 async fn test_segments_tangent() {
13359 let initial_source = "\
13360sketch(on = XY) {
13361 line(start = [var 1, var 2], end = [var 3, var 4])
13362 arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
13363}
13364";
13365
13366 let program = Program::parse(initial_source).unwrap().0.unwrap();
13367
13368 let mut frontend = FrontendState::new();
13369
13370 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13371 let mock_ctx = ExecutorContext::new_mock(None).await;
13372 let version = Version(0);
13373
13374 frontend.hack_set_program(&ctx, program).await.unwrap();
13375 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13376 let sketch_id = sketch_object.id;
13377 let sketch = expect_sketch(sketch_object);
13378 let line1_id = *sketch.segments.get(2).unwrap();
13379 let arc1_id = *sketch.segments.get(6).unwrap();
13380
13381 let constraint = Constraint::Tangent(Tangent {
13382 input: vec![line1_id, arc1_id],
13383 });
13384 let (src_delta, scene_delta) = frontend
13385 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13386 .await
13387 .unwrap();
13388 assert_eq!(
13389 src_delta.text.as_str(),
13390 "\
13391sketch(on = XY) {
13392 line1 = line(start = [var 0.84, var 2.13], end = [var 3.82, var 3.27])
13393 arc1 = arc(start = [var 4.51, var 2.03], end = [var 7.05, var 2.02], center = [var 5.78, var 2.55])
13394 tangent([line1, arc1])
13395}
13396"
13397 );
13398 assert_eq!(
13399 scene_delta.new_graph.objects.len(),
13400 10,
13401 "{:#?}",
13402 scene_delta.new_graph.objects
13403 );
13404
13405 ctx.close().await;
13406 mock_ctx.close().await;
13407 }
13408
13409 #[tokio::test(flavor = "multi_thread")]
13410 async fn test_point_midpoint() {
13411 let initial_source = "\
13412sketch(on = XY) {
13413 point(at = [var 1, var 1])
13414 line(start = [var 0, var 0], end = [var 6, var 4])
13415}
13416";
13417
13418 let program = Program::parse(initial_source).unwrap().0.unwrap();
13419
13420 let mut frontend = FrontendState::new();
13421
13422 let ctx = ExecutorContext::new_mock(None).await;
13423 let version = Version(0);
13424
13425 frontend.program = program.clone();
13426 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
13427 frontend.update_state_after_exec(outcome, true);
13428 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13429 let sketch_id = sketch_object.id;
13430 let sketch = expect_sketch(sketch_object);
13431 let point_id = *sketch.segments.first().unwrap();
13432 let line_id = *sketch.segments.get(3).unwrap();
13433
13434 let constraint = Constraint::Midpoint(Midpoint {
13435 point: ConstraintSegment::from(point_id),
13436 segment: line_id,
13437 });
13438 let (src_delta, scene_delta) = frontend
13439 .add_constraint(&ctx, version, sketch_id, constraint)
13440 .await
13441 .unwrap();
13442 assert_eq!(
13443 src_delta.text.as_str(),
13444 "\
13445sketch(on = XY) {
13446 point1 = point(at = [var 2.33, var 1.67])
13447 line1 = line(start = [var -0.67, var -0.33], end = [var 5.33, var 3.67])
13448 midpoint(line1, point = point1)
13449}
13450"
13451 );
13452 assert_eq!(
13453 scene_delta.new_graph.objects.len(),
13454 7,
13455 "{:#?}",
13456 scene_delta.new_graph.objects
13457 );
13458
13459 ctx.close().await;
13460 }
13461
13462 #[tokio::test(flavor = "multi_thread")]
13463 async fn test_segments_symmetric() {
13464 let initial_source = "\
13465sketch(on = XY) {
13466 line(start = [var 0, var 0], end = [var 0, var 4])
13467 line(start = [var 4, var 0], end = [var 4, var 4])
13468 line(start = [var 2, var -1], end = [var 2, var 5])
13469}
13470";
13471
13472 let program = Program::parse(initial_source).unwrap().0.unwrap();
13473
13474 let mut frontend = FrontendState::new();
13475
13476 let ctx = ExecutorContext::new_mock(None).await;
13477 let version = Version(0);
13478
13479 frontend.program = program.clone();
13480 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
13481 frontend.update_state_after_exec(outcome, true);
13482 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13483 let sketch_id = sketch_object.id;
13484 let sketch = expect_sketch(sketch_object);
13485 let line1_id = *sketch.segments.get(2).unwrap();
13486 let line2_id = *sketch.segments.get(5).unwrap();
13487 let axis_id = *sketch.segments.get(8).unwrap();
13488
13489 let constraint = Constraint::Symmetric(Symmetric {
13490 input: vec![line1_id, line2_id],
13491 axis: axis_id,
13492 });
13493 let (src_delta, scene_delta) = frontend
13494 .add_constraint(&ctx, version, sketch_id, constraint)
13495 .await
13496 .unwrap();
13497 assert_eq!(
13498 src_delta.text.as_str(),
13499 "\
13500sketch(on = XY) {
13501 line1 = line(start = [var 0, var 0], end = [var 0, var 4])
13502 line2 = line(start = [var 4, var 0], end = [var 4, var 4])
13503 line3 = line(start = [var 2, var -1], end = [var 2, var 5])
13504 symmetric([line1, line2], axis = line3)
13505}
13506"
13507 );
13508 assert_eq!(
13509 scene_delta.new_graph.objects.len(),
13510 12,
13511 "{:#?}",
13512 scene_delta.new_graph.objects
13513 );
13514
13515 ctx.close().await;
13516 }
13517
13518 #[tokio::test(flavor = "multi_thread")]
13519 async fn test_point_arc_midpoint() {
13520 let initial_source = "\
13521sketch(on = XY) {
13522 point(at = [var 6, var 3])
13523 arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
13524}
13525";
13526
13527 let program = Program::parse(initial_source).unwrap().0.unwrap();
13528
13529 let mut frontend = FrontendState::new();
13530
13531 let ctx = ExecutorContext::new_mock(None).await;
13532 let version = Version(0);
13533
13534 frontend.program = program.clone();
13535 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
13536 frontend.update_state_after_exec(outcome, true);
13537 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13538 let sketch_id = sketch_object.id;
13539 let sketch = expect_sketch(sketch_object);
13540 let point_id = *sketch.segments.first().unwrap();
13541 let arc_id = *sketch.segments.get(4).unwrap();
13542
13543 let constraint = Constraint::Midpoint(Midpoint {
13544 point: ConstraintSegment::from(point_id),
13545 segment: arc_id,
13546 });
13547 let (src_delta, scene_delta) = frontend
13548 .add_constraint(&ctx, version, sketch_id, constraint)
13549 .await
13550 .unwrap();
13551 assert_eq!(
13552 src_delta.text.as_str(),
13553 "\
13554sketch(on = XY) {
13555 point1 = point(at = [var 6, var 2.35])
13556 arc1 = arc(start = [var 6, var 2.35], end = [var 6, var 2.35], center = [var 6, var 1.94])
13557 midpoint(arc1, point = point1)
13558}
13559"
13560 );
13561 assert_eq!(
13562 scene_delta.new_graph.objects.len(),
13563 8,
13564 "{:#?}",
13565 scene_delta.new_graph.objects
13566 );
13567
13568 ctx.close().await;
13569 }
13570
13571 #[tokio::test(flavor = "multi_thread")]
13572 async fn test_origin_line_midpoint() {
13573 let initial_source = "\
13574sketch(on = XY) {
13575 line(start = [var 0, var 0], end = [var 6, var 4])
13576}
13577";
13578
13579 let program = Program::parse(initial_source).unwrap().0.unwrap();
13580
13581 let mut frontend = FrontendState::new();
13582
13583 let ctx = ExecutorContext::new_mock(None).await;
13584 let version = Version(0);
13585
13586 frontend.program = program.clone();
13587 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
13588 frontend.update_state_after_exec(outcome, true);
13589 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13590 let sketch_id = sketch_object.id;
13591 let sketch = expect_sketch(sketch_object);
13592 let line_id = *sketch.segments.get(2).unwrap();
13593
13594 let constraint = Constraint::Midpoint(Midpoint {
13595 point: ConstraintSegment::ORIGIN,
13596 segment: line_id,
13597 });
13598 let (src_delta, scene_delta) = frontend
13599 .add_constraint(&ctx, version, sketch_id, constraint)
13600 .await
13601 .unwrap();
13602 assert_eq!(
13603 src_delta.text.as_str(),
13604 "\
13605sketch(on = XY) {
13606 line1 = line(start = [var -3, var -2], end = [var 3, var 2])
13607 midpoint(line1, point = ORIGIN)
13608}
13609"
13610 );
13611 assert_eq!(
13612 scene_delta.new_graph.objects.len(),
13613 6,
13614 "{:#?}",
13615 scene_delta.new_graph.objects
13616 );
13617
13618 ctx.close().await;
13619 }
13620
13621 #[tokio::test(flavor = "multi_thread")]
13622 async fn test_origin_arc_midpoint() {
13623 let initial_source = "\
13624sketch(on = XY) {
13625 arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
13626}
13627";
13628
13629 let program = Program::parse(initial_source).unwrap().0.unwrap();
13630
13631 let mut frontend = FrontendState::new();
13632
13633 let ctx = ExecutorContext::new_mock(None).await;
13634 let version = Version(0);
13635
13636 frontend.program = program.clone();
13637 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
13638 frontend.update_state_after_exec(outcome, true);
13639 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13640 let sketch_id = sketch_object.id;
13641 let sketch = expect_sketch(sketch_object);
13642 let arc_id = *sketch.segments.get(3).unwrap();
13643
13644 let constraint = Constraint::Midpoint(Midpoint {
13645 point: ConstraintSegment::ORIGIN,
13646 segment: arc_id,
13647 });
13648 let (src_delta, scene_delta) = frontend
13649 .add_constraint(&ctx, version, sketch_id, constraint)
13650 .await
13651 .unwrap();
13652 assert_eq!(
13653 src_delta.text.as_str(),
13654 "\
13655sketch(on = XY) {
13656 arc1 = arc(start = [var 0.35, var 2.24], end = [var 1.62, var -1.58], center = [var 2.34, var 0.78])
13657 midpoint(arc1, point = ORIGIN)
13658}
13659"
13660 );
13661 assert_eq!(
13662 scene_delta.new_graph.objects.len(),
13663 7,
13664 "{:#?}",
13665 scene_delta.new_graph.objects
13666 );
13667
13668 ctx.close().await;
13669 }
13670
13671 #[tokio::test(flavor = "multi_thread")]
13672 async fn test_segments_symmetric_arcs() {
13673 let initial_source = "\
13674sketch(on = XY) {
13675 arc(start = [var -15, var 0], end = [var -10, var 5], center = [var -10, var 0])
13676 arc(start = [var 6, var 2], end = [var 12, var -4], center = [var 8, var 1])
13677 line(start = [var 0, var -10], end = [var 0, var 10])
13678}
13679";
13680
13681 let program = Program::parse(initial_source).unwrap().0.unwrap();
13682
13683 let mut frontend = FrontendState::new();
13684
13685 let ctx = ExecutorContext::new_mock(None).await;
13686 let version = Version(0);
13687
13688 frontend.program = program.clone();
13689 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
13690 frontend.update_state_after_exec(outcome, true);
13691 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13692 let sketch_id = sketch_object.id;
13693 let sketch = expect_sketch(sketch_object);
13694 let arc1_id = *sketch.segments.get(3).unwrap();
13695 let arc2_id = *sketch.segments.get(7).unwrap();
13696 let axis_id = *sketch.segments.get(10).unwrap();
13697
13698 let constraint = Constraint::Symmetric(Symmetric {
13699 input: vec![arc1_id, arc2_id],
13700 axis: axis_id,
13701 });
13702 let (src_delta, scene_delta) = frontend
13703 .add_constraint(&ctx, version, sketch_id, constraint)
13704 .await
13705 .unwrap();
13706 assert_eq!(
13707 src_delta.text.as_str(),
13708 "\
13709sketch(on = XY) {
13710 arc1 = arc(start = [var -14.46, var 0], end = [var -10, var 4.65], center = [var -10.14, var 0.31])
13711 arc2 = arc(start = [var 5.49, var 2.26], end = [var 11.58, var -3.47], center = [var 9.34, var 0.25])
13712 line1 = line(start = [var -0.44, var -10], end = [var -0.37, var 10])
13713 symmetric([arc1, arc2], axis = line1)
13714}
13715"
13716 );
13717 assert_eq!(
13718 scene_delta.new_graph.objects.len(),
13719 14,
13720 "{:#?}",
13721 scene_delta.new_graph.objects
13722 );
13723
13724 ctx.close().await;
13725 }
13726
13727 #[tokio::test(flavor = "multi_thread")]
13728 async fn test_sketch_on_face_simple() {
13729 let initial_source = "\
13730len = 2mm
13731cube = startSketchOn(XY)
13732 |> startProfile(at = [0, 0])
13733 |> line(end = [len, 0], tag = $side)
13734 |> line(end = [0, len])
13735 |> line(end = [-len, 0])
13736 |> line(end = [0, -len])
13737 |> close()
13738 |> extrude(length = len)
13739
13740face = faceOf(cube, face = side)
13741";
13742
13743 let program = Program::parse(initial_source).unwrap().0.unwrap();
13744
13745 let mut frontend = FrontendState::new();
13746
13747 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13748 let mock_ctx = ExecutorContext::new_mock(None).await;
13749 let version = Version(0);
13750
13751 frontend.hack_set_program(&ctx, program).await.unwrap();
13752 let face_object = find_first_face_object(&frontend.scene_graph).unwrap();
13753 let face_id = face_object.id;
13754
13755 let sketch_args = SketchCtor {
13756 on: Plane::Object(face_id),
13757 };
13758 let (_src_delta, scene_delta, sketch_id) = frontend
13759 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
13760 .await
13761 .unwrap();
13762 assert_eq!(sketch_id, ObjectId(2));
13763 assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
13764 let sketch_object = &scene_delta.new_graph.objects[2];
13765 assert_eq!(sketch_object.id, ObjectId(2));
13766 assert_eq!(
13767 sketch_object.kind,
13768 ObjectKind::Sketch(Sketch {
13769 args: SketchCtor {
13770 on: Plane::Object(face_id),
13771 },
13772 plane: face_id,
13773 segments: vec![],
13774 constraints: vec![],
13775 })
13776 );
13777 assert_eq!(scene_delta.new_graph.objects.len(), 8);
13778
13779 ctx.close().await;
13780 mock_ctx.close().await;
13781 }
13782
13783 #[tokio::test(flavor = "multi_thread")]
13784 async fn test_sketch_on_wall_artifact_from_region_extrude() {
13785 let initial_source = "\
13786s = sketch(on = YZ) {
13787 line1 = line(start = [0, 0], end = [0, 1])
13788 line2 = line(start = [0, 1], end = [1, 1])
13789 line3 = line(start = [1, 1], end = [0, 0])
13790}
13791region001 = region(point = [0.1, 0.1], sketch = s)
13792extrude001 = extrude(region001, length = 5)
13793";
13794
13795 let program = Program::parse(initial_source).unwrap().0.unwrap();
13796
13797 let mut frontend = FrontendState::new();
13798 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13799 let version = Version(0);
13800
13801 frontend.hack_set_program(&ctx, program).await.unwrap();
13802 let wall_object_id = find_first_wall_object_id(&frontend.scene_graph).expect("expected a wall object");
13803
13804 let sketch_args = SketchCtor {
13805 on: Plane::Object(wall_object_id),
13806 };
13807 let (src_delta, _scene_delta, _sketch_id) = frontend
13808 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
13809 .await
13810 .unwrap();
13811 assert!(src_delta.text.contains("faceOf(extrude001, face = region001.tags."));
13812
13813 ctx.close().await;
13814 }
13815
13816 #[tokio::test(flavor = "multi_thread")]
13817 async fn test_sketch_on_wall_artifact_from_split_region_extrude() {
13818 let initial_source = "\
13819sketch001 = sketch(on = YZ) {
13820 line1 = line(start = [var 0.49, var -0.39], end = [var 6.52, var -0.39])
13821 line2 = line(start = [var 6.52, var -0.39], end = [var 6.52, var 4.9])
13822 line3 = line(start = [var 6.52, var 4.9], end = [var 0.49, var 4.9])
13823 line4 = line(start = [var 0.49, var 4.9], end = [var 0.49, var -0.39])
13824 coincident([line1.end, line2.start])
13825 coincident([line2.end, line3.start])
13826 coincident([line3.end, line4.start])
13827 coincident([line4.end, line1.start])
13828 parallel([line2, line4])
13829 parallel([line3, line1])
13830 perpendicular([line1, line2])
13831 horizontal(line3)
13832 line5 = line(start = [2.35, 6.65], end = [5.89, -2.7])
13833}
13834region001 = region(point = [3.1, 3.74], sketch = sketch001)
13835extrude001 = extrude(region001, length = 5)
13836";
13837
13838 let program = Program::parse(initial_source).unwrap().0.unwrap();
13839
13840 let mut frontend = FrontendState::new();
13841 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13842 let version = Version(0);
13843
13844 frontend.hack_set_program(&ctx, program).await.unwrap();
13845 let wall_object_id = find_first_wall_object_id(&frontend.scene_graph).expect("expected a wall object");
13846
13847 let sketch_args = SketchCtor {
13848 on: Plane::Object(wall_object_id),
13849 };
13850 let (src_delta, _scene_delta, _sketch_id) = frontend
13851 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
13852 .await
13853 .unwrap();
13854 assert!(src_delta.text.contains("faceOf(extrude001, face = region001.tags."));
13855
13856 ctx.close().await;
13857 }
13858
13859 #[test]
13860 fn test_enclosing_variable_fallback_skips_nested_sketch_items() {
13861 let source = "\
13862sketch001 = sketch(on = XY) {
13863 line(start = [0, 0], end = [1, 0])
13864}
13865part = subtract(boxSolid, tools = [cutSolid])
13866 |> appearance(color = \"#8f96a3\")
13867";
13868 let ast = Program::parse(source).unwrap().0.unwrap().ast;
13869 let line_start = source.find("line").unwrap();
13870 let line_end = line_start + "line(start = [0, 0], end = [1, 0])".len();
13871 let line_ref = SourceRef::Simple {
13872 range: [line_start, line_end, 0].into(),
13873 node_path: None,
13874 };
13875 assert_eq!(variable_name_containing_source_ref(&ast, &line_ref), None);
13876
13877 let subtract_start = source.find("subtract").unwrap();
13878 let subtract_end = subtract_start + "subtract(boxSolid, tools = [cutSolid])".len();
13879 let subtract_ref = SourceRef::Simple {
13880 range: [subtract_start, subtract_end, 0].into(),
13881 node_path: None,
13882 };
13883 assert_eq!(
13884 variable_name_containing_source_ref(&ast, &subtract_ref),
13885 Some("part".to_owned())
13886 );
13887 }
13888
13889 #[tokio::test(flavor = "multi_thread")]
13890 async fn test_sketch_on_subtracted_sweep_cap_uses_composite_solid() {
13891 clear_mem_cache().await;
13892 let source = "\
13893boxSolid = startSketchOn(XY)
13894 |> startProfile(at = [0, 0])
13895 |> line(end = [4, 0], tag = $bottomEdge)
13896 |> line(end = [0, 4])
13897 |> line(end = [-4, 0])
13898 |> close()
13899 |> extrude(length = 10)
13900cutSolid = startSketchOn(XY)
13901 |> startProfile(at = [1, 1])
13902 |> line(end = [1, 0])
13903 |> line(end = [0, 1])
13904 |> line(end = [-1, 0])
13905 |> close()
13906 |> extrude(length = 10)
13907part = subtract(boxSolid, tools = [cutSolid])
13908 |> appearance(color = \"#8f96a3\", roughness = 55, metalness = 8)
13909";
13910 let program = Program::parse(source).unwrap().0.unwrap();
13911 let mut frontend = FrontendState::new();
13912 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13913 match frontend.hack_set_program(&ctx, program).await.unwrap() {
13914 SetProgramOutcome::Success { .. } => {}
13915 SetProgramOutcome::ExecFailure { error } => panic!("KCL fixture failed to execute: {error:?}"),
13916 }
13917
13918 let sweep_call_start = source.find("extrude").unwrap();
13919 let sweep_call_end = sweep_call_start + "extrude(length = 10)".len();
13920 let part_call_start = source.find("subtract").unwrap();
13921 let part_call_end = part_call_start + "subtract(boxSolid, tools = [cutSolid])".len();
13922 let sweep_range = [sweep_call_start, sweep_call_end, 0].into();
13923 let composite_range = [part_call_start, part_call_end, 0].into();
13924
13925 let cap_object = frontend
13926 .scene_graph
13927 .objects
13928 .iter()
13929 .find(|object| {
13930 matches!(
13931 object.kind,
13932 ObjectKind::Cap(crate::frontend::api::Cap {
13933 kind: crate::frontend::api::CapKind::End,
13934 ..
13935 })
13936 ) && matches!(
13937 &object.source,
13938 SourceRef::BackTrace { ranges }
13939 if ranges.len() == 2 && ranges[0].0 == composite_range && ranges[1].0 == sweep_range
13940 )
13941 })
13942 .expect("expected end cap object to trace through subtract and original extrude");
13943
13944 let mut ast = frontend.program.ast.clone();
13945 let cap_expr = sketch_on_ast_expr(&mut ast, &frontend.scene_graph, &Plane::Object(cap_object.id)).unwrap();
13946 let cap_face_decl = ast::VariableDeclaration::new(
13947 ast::VariableDeclarator::new("capFace", cap_expr.clone()),
13948 ast::ItemVisibility::Default,
13949 ast::VariableKind::Const,
13950 );
13951 ast.body
13952 .push(ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(
13953 cap_face_decl,
13954 ))));
13955 let generated_source = source_from_ast(&ast);
13956
13957 assert!(generated_source.contains("capFace = faceOf(part, face = END)"));
13958 assert!(!generated_source.contains("faceOf(boxSolid"));
13959 let ast::Expr::CallExpressionKw(call) = cap_expr else {
13960 panic!("expected faceOf call");
13961 };
13962 assert_eq!(call.callee.name.name, "faceOf");
13963 let ast::Expr::Name(solid_name) = call.unlabeled.as_ref().unwrap() else {
13964 panic!("expected solid name");
13965 };
13966 assert_eq!(solid_name.name.name, "part");
13967 let ast::Expr::Name(face_name) = &call.arguments[0].arg else {
13968 panic!("expected face name");
13969 };
13970 assert_eq!(face_name.name.name, "END");
13971
13972 ctx.close().await;
13973 }
13974
13975 #[tokio::test(flavor = "multi_thread")]
13976 async fn test_sketch_on_plane_incremental() {
13977 let initial_source = "\
13978len = 2mm
13979cube = startSketchOn(XY)
13980 |> startProfile(at = [0, 0])
13981 |> line(end = [len, 0], tag = $side)
13982 |> line(end = [0, len])
13983 |> line(end = [-len, 0])
13984 |> line(end = [0, -len])
13985 |> close()
13986 |> extrude(length = len)
13987
13988plane = planeOf(cube, face = side)
13989";
13990
13991 let program = Program::parse(initial_source).unwrap().0.unwrap();
13992
13993 let mut frontend = FrontendState::new();
13994
13995 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13996 let mock_ctx = ExecutorContext::new_mock(None).await;
13997 let version = Version(0);
13998
13999 frontend.hack_set_program(&ctx, program).await.unwrap();
14000 let plane_object = frontend
14002 .scene_graph
14003 .objects
14004 .iter()
14005 .rev()
14006 .find(|object| matches!(&object.kind, ObjectKind::Plane(_)))
14007 .unwrap();
14008 let plane_id = plane_object.id;
14009
14010 let sketch_args = SketchCtor {
14011 on: Plane::Object(plane_id),
14012 };
14013 let (src_delta, scene_delta, sketch_id) = frontend
14014 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14015 .await
14016 .unwrap();
14017 assert_eq!(
14018 src_delta.text.as_str(),
14019 "\
14020len = 2mm
14021cube = startSketchOn(XY)
14022 |> startProfile(at = [0, 0])
14023 |> line(end = [len, 0], tag = $side)
14024 |> line(end = [0, len])
14025 |> line(end = [-len, 0])
14026 |> line(end = [0, -len])
14027 |> close()
14028 |> extrude(length = len)
14029
14030plane = planeOf(cube, face = side)
14031sketch001 = sketch(on = plane) {
14032}
14033"
14034 );
14035 assert_eq!(sketch_id, ObjectId(2));
14036 assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
14037 let sketch_object = &scene_delta.new_graph.objects[2];
14038 assert_eq!(sketch_object.id, ObjectId(2));
14039 assert_eq!(
14040 sketch_object.kind,
14041 ObjectKind::Sketch(Sketch {
14042 args: SketchCtor {
14043 on: Plane::Object(plane_id),
14044 },
14045 plane: plane_id,
14046 segments: vec![],
14047 constraints: vec![],
14048 })
14049 );
14050 assert_eq!(scene_delta.new_graph.objects.len(), 9);
14051
14052 let plane_object = scene_delta.new_graph.objects.get(plane_id.0).unwrap();
14053 assert_eq!(plane_object.id, plane_id);
14054 assert_eq!(plane_object.kind, ObjectKind::Plane(Plane::Object(plane_id)));
14055
14056 ctx.close().await;
14057 mock_ctx.close().await;
14058 }
14059
14060 #[tokio::test(flavor = "multi_thread")]
14061 async fn test_new_sketch_uses_unique_variable_name() {
14062 let initial_source = "\
14063sketch1 = sketch(on = XY) {
14064}
14065";
14066
14067 let program = Program::parse(initial_source).unwrap().0.unwrap();
14068
14069 let mut frontend = FrontendState::new();
14070 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14071 let version = Version(0);
14072
14073 frontend.hack_set_program(&ctx, program).await.unwrap();
14074
14075 let sketch_args = SketchCtor {
14076 on: Plane::Default(PlaneName::Yz),
14077 };
14078 let (src_delta, _, _) = frontend
14079 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14080 .await
14081 .unwrap();
14082
14083 assert_eq!(
14084 src_delta.text.as_str(),
14085 "\
14086sketch1 = sketch(on = XY) {
14087}
14088sketch001 = sketch(on = YZ) {
14089}
14090"
14091 );
14092
14093 ctx.close().await;
14094 }
14095
14096 #[tokio::test(flavor = "multi_thread")]
14097 async fn test_new_sketch_twice_using_same_plane() {
14098 let initial_source = "\
14099sketch1 = sketch(on = XY) {
14100}
14101";
14102
14103 let program = Program::parse(initial_source).unwrap().0.unwrap();
14104
14105 let mut frontend = FrontendState::new();
14106 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14107 let version = Version(0);
14108
14109 frontend.hack_set_program(&ctx, program).await.unwrap();
14110
14111 let sketch_args = SketchCtor {
14112 on: Plane::Default(PlaneName::Xy),
14113 };
14114 let (src_delta, _, _) = frontend
14115 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14116 .await
14117 .unwrap();
14118
14119 assert_eq!(
14120 src_delta.text.as_str(),
14121 "\
14122sketch1 = sketch(on = XY) {
14123}
14124sketch001 = sketch(on = XY) {
14125}
14126"
14127 );
14128
14129 ctx.close().await;
14130 }
14131
14132 #[tokio::test(flavor = "multi_thread")]
14133 async fn test_sketch_mode_reuses_cached_on_expression() {
14134 let initial_source = "\
14135width = 2mm
14136sketch(on = offsetPlane(XY, offset = width)) {
14137 line1 = line(start = [var 0, var 0], end = [var 1mm, var 0])
14138 distance([line1.start, line1.end]) == width
14139}
14140";
14141 let program = Program::parse(initial_source).unwrap().0.unwrap();
14142
14143 let mut frontend = FrontendState::new();
14144 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14145 let mock_ctx = ExecutorContext::new_mock(None).await;
14146 let version = Version(0);
14147 let project_id = ProjectId(0);
14148 let file_id = FileId(0);
14149
14150 frontend.hack_set_program(&ctx, program).await.unwrap();
14151 let initial_object_count = frontend.scene_graph.objects.len();
14152 let sketch_id = find_first_sketch_object(&frontend.scene_graph)
14153 .expect("Expected sketch object to exist")
14154 .id;
14155
14156 let scene_delta = frontend
14159 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
14160 .await
14161 .unwrap();
14162 assert_eq!(scene_delta.new_graph.objects.len(), initial_object_count);
14163
14164 let (_src_delta, scene_delta) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
14167 assert_eq!(scene_delta.new_graph.objects.len(), initial_object_count);
14168
14169 ctx.close().await;
14170 mock_ctx.close().await;
14171 }
14172
14173 #[tokio::test(flavor = "multi_thread")]
14174 async fn test_multiple_sketch_blocks() {
14175 let initial_source = "\
14176// Cube that requires the engine.
14177width = 2
14178sketch001 = startSketchOn(XY)
14179profile001 = startProfile(sketch001, at = [0, 0])
14180 |> yLine(length = width, tag = $seg1)
14181 |> xLine(length = width)
14182 |> yLine(length = -width)
14183 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
14184 |> close()
14185extrude001 = extrude(profile001, length = width)
14186
14187// Get a value that requires the engine.
14188x = segLen(seg1)
14189
14190// Triangle with side length 2*x.
14191sketch(on = XY) {
14192 line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
14193 line2 = line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
14194 coincident([line1.end, line2.start])
14195 line3 = line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
14196 coincident([line2.end, line3.start])
14197 coincident([line3.end, line1.start])
14198 equalLength([line3, line1])
14199 equalLength([line1, line2])
14200 distance([line1.start, line1.end]) == 2*x
14201}
14202
14203// Line segment with length x.
14204sketch2 = sketch(on = XY) {
14205 line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
14206 distance([line1.start, line1.end]) == x
14207}
14208";
14209
14210 let program = Program::parse(initial_source).unwrap().0.unwrap();
14211
14212 let mut frontend = FrontendState::new();
14213
14214 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14215 let mock_ctx = ExecutorContext::new_mock(None).await;
14216 let version = Version(0);
14217 let project_id = ProjectId(0);
14218 let file_id = FileId(0);
14219
14220 frontend.hack_set_program(&ctx, program).await.unwrap();
14221 let sketch_objects = frontend
14222 .scene_graph
14223 .objects
14224 .iter()
14225 .filter(|obj| matches!(obj.kind, ObjectKind::Sketch(_)))
14226 .collect::<Vec<_>>();
14227 let sketch1_id = sketch_objects.first().unwrap().id;
14228 let sketch2_id = sketch_objects.get(1).unwrap().id;
14229 let point1_id = ObjectId(sketch1_id.0 + 1);
14231 let point2_id = ObjectId(sketch2_id.0 + 1);
14233
14234 let scene_delta = frontend
14243 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch1_id)
14244 .await
14245 .unwrap();
14246 assert_eq!(
14247 scene_delta.new_graph.objects.len(),
14248 18,
14249 "{:#?}",
14250 scene_delta.new_graph.objects
14251 );
14252
14253 let point_ctor = PointCtor {
14255 position: Point2d {
14256 x: Expr::Var(Number {
14257 value: 1.0,
14258 units: NumericSuffix::Mm,
14259 }),
14260 y: Expr::Var(Number {
14261 value: 2.0,
14262 units: NumericSuffix::Mm,
14263 }),
14264 },
14265 };
14266 let segments = vec![ExistingSegmentCtor {
14267 id: point1_id,
14268 ctor: SegmentCtor::Point(point_ctor),
14269 }];
14270 let (src_delta, _) = frontend
14271 .edit_segments(&mock_ctx, version, sketch1_id, segments)
14272 .await
14273 .unwrap();
14274 assert_eq!(
14276 src_delta.text.as_str(),
14277 "\
14278// Cube that requires the engine.
14279width = 2
14280sketch001 = startSketchOn(XY)
14281profile001 = startProfile(sketch001, at = [0, 0])
14282 |> yLine(length = width, tag = $seg1)
14283 |> xLine(length = width)
14284 |> yLine(length = -width)
14285 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
14286 |> close()
14287extrude001 = extrude(profile001, length = width)
14288
14289// Get a value that requires the engine.
14290x = segLen(seg1)
14291
14292// Triangle with side length 2*x.
14293sketch(on = XY) {
14294 line1 = line(start = [var 1mm, var 2mm], end = [var 2.32mm, var -1.78mm])
14295 line2 = line(start = [var 2.32mm, var -1.78mm], end = [var -1.61mm, var -1.03mm])
14296 coincident([line1.end, line2.start])
14297 line3 = line(start = [var -1.61mm, var -1.03mm], end = [var 1mm, var 2mm])
14298 coincident([line2.end, line3.start])
14299 coincident([line3.end, line1.start])
14300 equalLength([line3, line1])
14301 equalLength([line1, line2])
14302 distance([line1.start, line1.end]) == 2 * x
14303}
14304
14305// Line segment with length x.
14306sketch2 = sketch(on = XY) {
14307 line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
14308 distance([line1.start, line1.end]) == x
14309}
14310"
14311 );
14312 let edited_sketch1_source = src_delta.text.clone();
14313
14314 let (src_delta, _) = frontend.execute_mock(&mock_ctx, version, sketch1_id).await.unwrap();
14316 assert_eq!(src_delta.text, edited_sketch1_source);
14317 let scene = frontend.exit_sketch(&ctx, version, sketch1_id).await.unwrap();
14325 assert_eq!(scene.objects.len(), 30, "{:#?}", scene.objects);
14326
14327 let scene_delta = frontend
14335 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch2_id)
14336 .await
14337 .unwrap();
14338 assert_eq!(
14339 scene_delta.new_graph.objects.len(),
14340 24,
14341 "{:#?}",
14342 scene_delta.new_graph.objects
14343 );
14344
14345 let point_ctor = PointCtor {
14347 position: Point2d {
14348 x: Expr::Var(Number {
14349 value: 3.0,
14350 units: NumericSuffix::Mm,
14351 }),
14352 y: Expr::Var(Number {
14353 value: 4.0,
14354 units: NumericSuffix::Mm,
14355 }),
14356 },
14357 };
14358 let segments = vec![ExistingSegmentCtor {
14359 id: point2_id,
14360 ctor: SegmentCtor::Point(point_ctor),
14361 }];
14362 let (src_delta, _) = frontend
14363 .edit_segments(&mock_ctx, version, sketch2_id, segments)
14364 .await
14365 .unwrap();
14366 assert_eq!(
14368 src_delta.text.as_str(),
14369 "\
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 1mm, var 2mm], end = [var 2.32mm, var -1.78mm])
14387 line2 = line(start = [var 2.32mm, var -1.78mm], end = [var -1.61mm, var -1.03mm])
14388 coincident([line1.end, line2.start])
14389 line3 = line(start = [var -1.61mm, var -1.03mm], end = [var 1mm, var 2mm])
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 3mm, var 4mm], end = [var 2.32mm, var 2.12mm])
14400 distance([line1.start, line1.end]) == x
14401}
14402"
14403 );
14404 let edited_sketch2_source = src_delta.text.clone();
14405
14406 let (src_delta, _) = frontend.execute_mock(&mock_ctx, version, sketch2_id).await.unwrap();
14408 assert_eq!(src_delta.text, edited_sketch2_source);
14409
14410 ctx.close().await;
14411 mock_ctx.close().await;
14412 }
14413
14414 #[tokio::test(flavor = "multi_thread")]
14415 async fn test_exit_sketch_without_changes_allows_entering_next_sketch() {
14416 clear_mem_cache().await;
14417
14418 let source = r#"sketch001 = sketch(on = XZ) {
14419 circle1 = circle(start = [var -1.96mm, var 2.77mm], center = [var -2.69mm, var 3.44mm])
14420}
14421sketch002 = sketch(on = XY) {
14422 line1 = line(start = [var 0mm, var 0mm], end = [var 4.68mm, var 0mm])
14423 line2 = line(start = [var 4.68mm, var 0mm], end = [var 4.68mm, var 2.96mm])
14424 line3 = line(start = [var 4.68mm, var 2.96mm], end = [var 0mm, var 2.96mm])
14425 line4 = line(start = [var 0mm, var 2.96mm], end = [var 0mm, var 0mm])
14426 coincident([line1.end, line2.start])
14427 coincident([line2.end, line3.start])
14428 coincident([line3.end, line4.start])
14429 coincident([line4.end, line1.start])
14430 parallel([line2, line4])
14431 parallel([line3, line1])
14432 perpendicular([line1, line2])
14433 horizontal(line3)
14434 coincident([line1.start, ORIGIN])
14435}
14436"#;
14437
14438 let program = Program::parse(source).unwrap().0.unwrap();
14439 let mut frontend = FrontendState::new();
14440 let ctx = ExecutorContext::new_with_engine(sync::Arc::new(EngineManager::new_mock()), Default::default());
14441 let mock_ctx = ExecutorContext::new_mock(None).await;
14442 let version = Version(0);
14443 let project_id = ProjectId(0);
14444 let file_id = FileId(0);
14445
14446 frontend.hack_set_program(&ctx, program).await.unwrap();
14447 let sketch_objects = frontend
14448 .scene_graph
14449 .objects
14450 .iter()
14451 .filter(|object| matches!(object.kind, ObjectKind::Sketch(_)))
14452 .collect::<Vec<_>>();
14453 assert_eq!(sketch_objects.len(), 2, "{:#?}", frontend.scene_graph.objects);
14454
14455 let sketch1_id = sketch_objects[0].id;
14456 let sketch2_id = sketch_objects[1].id;
14457
14458 frontend
14459 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch1_id)
14460 .await
14461 .unwrap();
14462 frontend.exit_sketch(&ctx, version, sketch1_id).await.unwrap();
14463
14464 let scene_delta = frontend
14465 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch2_id)
14466 .await
14467 .unwrap();
14468 assert_eq!(scene_delta.new_graph.sketch_mode, Some(sketch2_id));
14469
14470 clear_mem_cache().await;
14471 ctx.close().await;
14472 mock_ctx.close().await;
14473 }
14474
14475 #[tokio::test(flavor = "multi_thread")]
14480 async fn test_extra_newlines_after_settings_edit_sketch_add_point() {
14481 let initial_source = "@settings(defaultLengthUnit = mm)
14483
14484
14485
14486sketch001 = sketch(on = XY) {
14487 point(at = [1in, 2in])
14488}
14489";
14490
14491 let program = Program::parse(initial_source).unwrap().0.unwrap();
14492 let mut frontend = FrontendState::new();
14493
14494 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14495 let mock_ctx = ExecutorContext::new_mock(None).await;
14496 let version = Version(0);
14497 let project_id = ProjectId(0);
14498 let file_id = FileId(0);
14499
14500 frontend.hack_set_program(&ctx, program).await.unwrap();
14501 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14502 let sketch_id = sketch_object.id;
14503
14504 frontend
14506 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
14507 .await
14508 .unwrap();
14509
14510 let point_ctor = PointCtor {
14512 position: Point2d {
14513 x: Expr::Number(Number {
14514 value: 5.0,
14515 units: NumericSuffix::Mm,
14516 }),
14517 y: Expr::Number(Number {
14518 value: 6.0,
14519 units: NumericSuffix::Mm,
14520 }),
14521 },
14522 };
14523 let segment = SegmentCtor::Point(point_ctor);
14524 let (src_delta, scene_delta) = frontend
14525 .add_segment(&mock_ctx, version, sketch_id, segment, None)
14526 .await
14527 .unwrap();
14528 assert!(
14530 src_delta.text.contains("point(at = [5mm, 6mm])"),
14531 "Expected new point in source, got: {}",
14532 src_delta.text
14533 );
14534 assert!(!scene_delta.new_objects.is_empty());
14535
14536 ctx.close().await;
14537 mock_ctx.close().await;
14538 }
14539
14540 #[tokio::test(flavor = "multi_thread")]
14541 async fn test_ensure_control_point_spline_experimental_features_adds_allow_setting() {
14542 let initial_program = Program::parse("s = sketch(on = XY) {}\n").unwrap().0.unwrap();
14543
14544 let updated_program = ensure_control_point_spline_experimental_features(&initial_program).unwrap();
14545 let meta_settings = updated_program.meta_settings().unwrap().unwrap();
14546
14547 assert_eq!(meta_settings.experimental_features, WarningLevel::Allow);
14548 assert!(
14549 source_from_ast(&updated_program.ast).contains("@settings(experimentalFeatures = allow)"),
14550 "Expected experimental settings to be added to source"
14551 );
14552 }
14553
14554 #[tokio::test(flavor = "multi_thread")]
14555 async fn test_extra_newlines_after_settings_add_line_to_empty_sketch() {
14556 let initial_source = "@settings(defaultLengthUnit = mm)
14558
14559
14560
14561s = sketch(on = XY) {}
14562";
14563
14564 let program = Program::parse(initial_source).unwrap().0.unwrap();
14565 let mut frontend = FrontendState::new();
14566
14567 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14568 let mock_ctx = ExecutorContext::new_mock(None).await;
14569 let version = Version(0);
14570
14571 frontend.hack_set_program(&ctx, program).await.unwrap();
14572 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14573 let sketch_id = sketch_object.id;
14574
14575 let line_ctor = LineCtor {
14576 start: Point2d {
14577 x: Expr::Number(Number {
14578 value: 0.0,
14579 units: NumericSuffix::Mm,
14580 }),
14581 y: Expr::Number(Number {
14582 value: 0.0,
14583 units: NumericSuffix::Mm,
14584 }),
14585 },
14586 end: Point2d {
14587 x: Expr::Number(Number {
14588 value: 10.0,
14589 units: NumericSuffix::Mm,
14590 }),
14591 y: Expr::Number(Number {
14592 value: 10.0,
14593 units: NumericSuffix::Mm,
14594 }),
14595 },
14596 construction: None,
14597 };
14598 let segment = SegmentCtor::Line(line_ctor);
14599 let (src_delta, scene_delta) = frontend
14600 .add_segment(&mock_ctx, version, sketch_id, segment, None)
14601 .await
14602 .unwrap();
14603 assert!(
14604 src_delta.text.contains("line(start = [0mm, 0mm], end = [10mm, 10mm])"),
14605 "Expected line in source, got: {}",
14606 src_delta.text
14607 );
14608 assert_eq!(scene_delta.new_objects.len(), 3);
14610
14611 ctx.close().await;
14612 mock_ctx.close().await;
14613 }
14614
14615 #[tokio::test(flavor = "multi_thread")]
14616 async fn test_extra_newlines_between_operations_edit_line() {
14617 let initial_source = "@settings(defaultLengthUnit = mm)
14619
14620
14621sketch001 = sketch(on = XY) {
14622
14623 line1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm])
14624
14625}
14626";
14627
14628 let program = Program::parse(initial_source).unwrap().0.unwrap();
14629 let mut frontend = FrontendState::new();
14630
14631 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14632 let mock_ctx = ExecutorContext::new_mock(None).await;
14633 let version = Version(0);
14634 let project_id = ProjectId(0);
14635 let file_id = FileId(0);
14636
14637 frontend.hack_set_program(&ctx, program).await.unwrap();
14638 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14639 let sketch_id = sketch_object.id;
14640 let sketch = expect_sketch(sketch_object);
14641
14642 let line_id = sketch
14644 .segments
14645 .iter()
14646 .copied()
14647 .find(|seg_id| {
14648 matches!(
14649 &frontend.scene_graph.objects[seg_id.0].kind,
14650 ObjectKind::Segment {
14651 segment: Segment::Line(_)
14652 }
14653 )
14654 })
14655 .expect("Expected a line segment in sketch");
14656
14657 frontend
14659 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
14660 .await
14661 .unwrap();
14662
14663 let line_ctor = LineCtor {
14665 start: Point2d {
14666 x: Expr::Var(Number {
14667 value: 1.0,
14668 units: NumericSuffix::Mm,
14669 }),
14670 y: Expr::Var(Number {
14671 value: 2.0,
14672 units: NumericSuffix::Mm,
14673 }),
14674 },
14675 end: Point2d {
14676 x: Expr::Var(Number {
14677 value: 13.0,
14678 units: NumericSuffix::Mm,
14679 }),
14680 y: Expr::Var(Number {
14681 value: 14.0,
14682 units: NumericSuffix::Mm,
14683 }),
14684 },
14685 construction: None,
14686 };
14687 let segments = vec![ExistingSegmentCtor {
14688 id: line_id,
14689 ctor: SegmentCtor::Line(line_ctor),
14690 }];
14691 let (src_delta, _scene_delta) = frontend
14692 .edit_segments(&mock_ctx, version, sketch_id, segments)
14693 .await
14694 .unwrap();
14695 assert!(
14696 src_delta
14697 .text
14698 .contains("line(start = [var 1mm, var 2mm], end = [var 13mm, var 14mm])"),
14699 "Expected edited line in source, got: {}",
14700 src_delta.text
14701 );
14702
14703 ctx.close().await;
14704 mock_ctx.close().await;
14705 }
14706
14707 #[tokio::test(flavor = "multi_thread")]
14708 async fn test_extra_newlines_delete_segment() {
14709 let initial_source = "@settings(defaultLengthUnit = mm)
14711
14712
14713
14714sketch001 = sketch(on = XY) {
14715 circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
14716}
14717";
14718
14719 let program = Program::parse(initial_source).unwrap().0.unwrap();
14720 let mut frontend = FrontendState::new();
14721
14722 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14723 let mock_ctx = ExecutorContext::new_mock(None).await;
14724 let version = Version(0);
14725
14726 frontend.hack_set_program(&ctx, program).await.unwrap();
14727 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14728 let sketch_id = sketch_object.id;
14729 let sketch = expect_sketch(sketch_object);
14730
14731 assert_eq!(sketch.segments.len(), 3);
14733 let circle_id = sketch.segments[2];
14734
14735 let (src_delta, scene_delta) = frontend
14737 .delete_objects(&mock_ctx, version, sketch_id, vec![], vec![circle_id])
14738 .await
14739 .unwrap();
14740 assert!(
14741 src_delta.text.contains("sketch(on = XY) {"),
14742 "Expected sketch block in source, got: {}",
14743 src_delta.text
14744 );
14745 let new_sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
14746 let new_sketch = expect_sketch(new_sketch_object);
14747 assert_eq!(new_sketch.segments.len(), 0);
14748
14749 ctx.close().await;
14750 mock_ctx.close().await;
14751 }
14752
14753 #[tokio::test(flavor = "multi_thread")]
14754 async fn test_unformatted_source_add_arc() {
14755 let initial_source = "@settings(defaultLengthUnit = mm)
14757
14758
14759
14760
14761sketch001 = sketch(on = XY) {
14762}
14763";
14764
14765 let program = Program::parse(initial_source).unwrap().0.unwrap();
14766 let mut frontend = FrontendState::new();
14767
14768 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14769 let mock_ctx = ExecutorContext::new_mock(None).await;
14770 let version = Version(0);
14771
14772 frontend.hack_set_program(&ctx, program).await.unwrap();
14773 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14774 let sketch_id = sketch_object.id;
14775
14776 let arc_ctor = ArcCtor {
14777 start: Point2d {
14778 x: Expr::Var(Number {
14779 value: 5.0,
14780 units: NumericSuffix::Mm,
14781 }),
14782 y: Expr::Var(Number {
14783 value: 0.0,
14784 units: NumericSuffix::Mm,
14785 }),
14786 },
14787 end: Point2d {
14788 x: Expr::Var(Number {
14789 value: 0.0,
14790 units: NumericSuffix::Mm,
14791 }),
14792 y: Expr::Var(Number {
14793 value: 5.0,
14794 units: NumericSuffix::Mm,
14795 }),
14796 },
14797 center: Point2d {
14798 x: Expr::Var(Number {
14799 value: 0.0,
14800 units: NumericSuffix::Mm,
14801 }),
14802 y: Expr::Var(Number {
14803 value: 0.0,
14804 units: NumericSuffix::Mm,
14805 }),
14806 },
14807 construction: None,
14808 };
14809 let segment = SegmentCtor::Arc(arc_ctor);
14810 let (src_delta, scene_delta) = frontend
14811 .add_segment(&mock_ctx, version, sketch_id, segment, None)
14812 .await
14813 .unwrap();
14814 assert!(
14815 src_delta
14816 .text
14817 .contains("arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])"),
14818 "Expected arc in source, got: {}",
14819 src_delta.text
14820 );
14821 assert!(!scene_delta.new_objects.is_empty());
14822
14823 ctx.close().await;
14824 mock_ctx.close().await;
14825 }
14826
14827 #[tokio::test(flavor = "multi_thread")]
14828 async fn test_extra_newlines_add_circle() {
14829 let initial_source = "@settings(defaultLengthUnit = mm)
14831
14832
14833
14834sketch001 = sketch(on = XY) {
14835}
14836";
14837
14838 let program = Program::parse(initial_source).unwrap().0.unwrap();
14839 let mut frontend = FrontendState::new();
14840
14841 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14842 let mock_ctx = ExecutorContext::new_mock(None).await;
14843 let version = Version(0);
14844
14845 frontend.hack_set_program(&ctx, program).await.unwrap();
14846 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14847 let sketch_id = sketch_object.id;
14848
14849 let circle_ctor = CircleCtor {
14850 start: Point2d {
14851 x: Expr::Var(Number {
14852 value: 5.0,
14853 units: NumericSuffix::Mm,
14854 }),
14855 y: Expr::Var(Number {
14856 value: 0.0,
14857 units: NumericSuffix::Mm,
14858 }),
14859 },
14860 center: Point2d {
14861 x: Expr::Var(Number {
14862 value: 0.0,
14863 units: NumericSuffix::Mm,
14864 }),
14865 y: Expr::Var(Number {
14866 value: 0.0,
14867 units: NumericSuffix::Mm,
14868 }),
14869 },
14870 construction: None,
14871 };
14872 let segment = SegmentCtor::Circle(circle_ctor);
14873 let (src_delta, scene_delta) = frontend
14874 .add_segment(&mock_ctx, version, sketch_id, segment, None)
14875 .await
14876 .unwrap();
14877 assert!(
14878 src_delta
14879 .text
14880 .contains("circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])"),
14881 "Expected circle in source, got: {}",
14882 src_delta.text
14883 );
14884 assert!(!scene_delta.new_objects.is_empty());
14885
14886 ctx.close().await;
14887 mock_ctx.close().await;
14888 }
14889
14890 #[tokio::test(flavor = "multi_thread")]
14891 async fn test_extra_newlines_add_constraint() {
14892 let initial_source = "@settings(defaultLengthUnit = mm)
14894
14895
14896
14897sketch001 = sketch(on = XY) {
14898 line1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm])
14899 line2 = line(start = [var 10mm, var 10mm], end = [var 20mm, var 0mm])
14900}
14901";
14902
14903 let program = Program::parse(initial_source).unwrap().0.unwrap();
14904 let mut frontend = FrontendState::new();
14905
14906 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14907 let mock_ctx = ExecutorContext::new_mock(None).await;
14908 let version = Version(0);
14909 let project_id = ProjectId(0);
14910 let file_id = FileId(0);
14911
14912 frontend.hack_set_program(&ctx, program).await.unwrap();
14913 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14914 let sketch_id = sketch_object.id;
14915 let sketch = expect_sketch(sketch_object);
14916
14917 let line_ids: Vec<ObjectId> = sketch
14919 .segments
14920 .iter()
14921 .copied()
14922 .filter(|seg_id| {
14923 matches!(
14924 &frontend.scene_graph.objects[seg_id.0].kind,
14925 ObjectKind::Segment {
14926 segment: Segment::Line(_)
14927 }
14928 )
14929 })
14930 .collect();
14931 assert_eq!(line_ids.len(), 2, "Expected two line segments");
14932
14933 let line1 = &frontend.scene_graph.objects[line_ids[0].0];
14934 let ObjectKind::Segment {
14935 segment: Segment::Line(line1_data),
14936 } = &line1.kind
14937 else {
14938 panic!("Expected line");
14939 };
14940 let line2 = &frontend.scene_graph.objects[line_ids[1].0];
14941 let ObjectKind::Segment {
14942 segment: Segment::Line(line2_data),
14943 } = &line2.kind
14944 else {
14945 panic!("Expected line");
14946 };
14947
14948 let constraint = Constraint::Coincident(Coincident {
14950 segments: vec![line1_data.end.into(), line2_data.start.into()],
14951 });
14952
14953 frontend
14955 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
14956 .await
14957 .unwrap();
14958 let (src_delta, _scene_delta) = frontend
14959 .add_constraint(&mock_ctx, version, sketch_id, constraint)
14960 .await
14961 .unwrap();
14962 assert!(
14963 src_delta.text.contains("coincident("),
14964 "Expected coincident constraint in source, got: {}",
14965 src_delta.text
14966 );
14967
14968 ctx.close().await;
14969 mock_ctx.close().await;
14970 }
14971
14972 #[tokio::test(flavor = "multi_thread")]
14973 async fn test_extra_newlines_add_line_then_edit_line() {
14974 let initial_source = "@settings(defaultLengthUnit = mm)
14976
14977
14978
14979sketch001 = sketch(on = XY) {
14980}
14981";
14982
14983 let program = Program::parse(initial_source).unwrap().0.unwrap();
14984 let mut frontend = FrontendState::new();
14985
14986 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14987 let mock_ctx = ExecutorContext::new_mock(None).await;
14988 let version = Version(0);
14989
14990 frontend.hack_set_program(&ctx, program).await.unwrap();
14991 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14992 let sketch_id = sketch_object.id;
14993
14994 let line_ctor = LineCtor {
14996 start: Point2d {
14997 x: Expr::Number(Number {
14998 value: 0.0,
14999 units: NumericSuffix::Mm,
15000 }),
15001 y: Expr::Number(Number {
15002 value: 0.0,
15003 units: NumericSuffix::Mm,
15004 }),
15005 },
15006 end: Point2d {
15007 x: Expr::Number(Number {
15008 value: 10.0,
15009 units: NumericSuffix::Mm,
15010 }),
15011 y: Expr::Number(Number {
15012 value: 10.0,
15013 units: NumericSuffix::Mm,
15014 }),
15015 },
15016 construction: None,
15017 };
15018 let segment = SegmentCtor::Line(line_ctor);
15019 let (src_delta, scene_delta) = frontend
15020 .add_segment(&mock_ctx, version, sketch_id, segment, None)
15021 .await
15022 .unwrap();
15023 assert!(
15024 src_delta.text.contains("line(start = [0mm, 0mm], end = [10mm, 10mm])"),
15025 "Expected line in source after add, got: {}",
15026 src_delta.text
15027 );
15028 let line_id = *scene_delta.new_objects.last().unwrap();
15030
15031 let line_ctor = LineCtor {
15033 start: Point2d {
15034 x: Expr::Number(Number {
15035 value: 1.0,
15036 units: NumericSuffix::Mm,
15037 }),
15038 y: Expr::Number(Number {
15039 value: 2.0,
15040 units: NumericSuffix::Mm,
15041 }),
15042 },
15043 end: Point2d {
15044 x: Expr::Number(Number {
15045 value: 13.0,
15046 units: NumericSuffix::Mm,
15047 }),
15048 y: Expr::Number(Number {
15049 value: 14.0,
15050 units: NumericSuffix::Mm,
15051 }),
15052 },
15053 construction: None,
15054 };
15055 let segments = vec![ExistingSegmentCtor {
15056 id: line_id,
15057 ctor: SegmentCtor::Line(line_ctor),
15058 }];
15059 let (src_delta, scene_delta) = frontend
15060 .edit_segments(&mock_ctx, version, sketch_id, segments)
15061 .await
15062 .unwrap();
15063 assert!(
15064 src_delta.text.contains("line(start = [1mm, 2mm], end = [13mm, 14mm])"),
15065 "Expected edited line in source, got: {}",
15066 src_delta.text
15067 );
15068 assert_eq!(scene_delta.new_objects, vec![]);
15069
15070 ctx.close().await;
15071 mock_ctx.close().await;
15072 }
15073}