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::Angle;
9use kcl_api::ArcCtor;
10use kcl_api::ArcDirection;
11use kcl_api::CapSource;
12use kcl_api::CircleCtor;
13use kcl_api::Coincident;
14use kcl_api::Constraint;
15use kcl_api::ConstraintSegment;
16use kcl_api::ControlPointSplineCtor;
17use kcl_api::Diameter;
18use kcl_api::Distance;
19use kcl_api::EqualRadius;
20use kcl_api::Expr;
21use kcl_api::FixedPoint;
22use kcl_api::Freedom;
23use kcl_api::Horizontal;
24use kcl_api::LineCtor;
25use kcl_api::LinesEqualLength;
26use kcl_api::Midpoint;
27use kcl_api::Number;
28use kcl_api::NumericSuffix;
29use kcl_api::Object;
30use kcl_api::ObjectId;
31use kcl_api::ObjectKind;
32use kcl_api::Parallel;
33use kcl_api::Perpendicular;
34use kcl_api::Plane;
35use kcl_api::Point2d;
36use kcl_api::PointCtor;
37use kcl_api::Radius;
38use kcl_api::Segment;
39use kcl_api::SegmentCtor;
40use kcl_api::SketchCtor;
41use kcl_api::SourceRef;
42use kcl_api::SourceRefRange;
43use kcl_api::Symmetric;
44use kcl_api::Tangent;
45use kcl_api::UnitLength;
46use kcl_api::Vertical;
47use kcl_api::WallSource;
48use kcl_error::CompilationIssue;
49use kcl_error::SourceRange;
50use serde::Serialize;
51use uuid::Uuid;
52
53use crate::ExecOutcome;
54use crate::ExecutorContext;
55use crate::KclError;
56use crate::KclErrorWithOutputs;
57use crate::KclValueView;
58use crate::Program;
59use crate::SegmentDragAnchor;
60use crate::collections::AhashIndexSet;
61use crate::execution::Artifact;
62use crate::execution::ArtifactGraph;
63use crate::execution::ArtifactId;
64use crate::execution::CapSubType;
65use crate::execution::CodeRef;
66use crate::execution::MockConfig;
67use crate::execution::SKETCH_BLOCK_PARAM_ON;
68use crate::execution::annotations::WarningLevel;
69use crate::execution::cache::SketchModeState;
70use crate::execution::types::adjust_length;
71use crate::fmt::format_number_literal;
72use crate::front::CoincidentExt;
73use crate::front::DistanceExt;
74use crate::front::Error;
75use crate::front::ExecResult;
76use crate::front::ObjectKindExt;
77use crate::front::SegmentExt;
78use crate::frontend::api::FileId;
79use crate::frontend::api::ProjectId;
80use crate::frontend::api::RestoreSketchCheckpointOutcome;
81use crate::frontend::api::SceneGraph;
82use crate::frontend::api::SceneGraphDelta;
83use crate::frontend::api::SketchCheckpointId;
84use crate::frontend::api::SourceDelta;
85use crate::frontend::api::Version;
86use crate::frontend::modify::find_defined_names;
87use crate::frontend::modify::next_free_name;
88use crate::frontend::modify::next_free_name_with_padding;
89use crate::frontend::sketch::ConstraintLabelPositionEdit;
90use crate::frontend::sketch::ExistingSegmentCtor;
91use crate::frontend::sketch::SketchApi;
92use crate::id::IncIdGenerator;
93use crate::parsing::ast::types as ast;
94use crate::parsing::ast::types::BoxNode;
95use crate::parsing::ast::types::CallExpressionKw;
96use crate::parsing::ast::types::NodePathExt;
97use crate::std::constraints::LinesAtAngleKind;
98use crate::walk::NodeMut;
99use crate::walk::Visitable;
100use crate::walk::traverse::MutateBodyItem;
101use crate::walk::traverse::TraversalReturn;
102use crate::walk::traverse::Visitor;
103use crate::walk::traverse::dfs_mut;
104
105pub(crate) mod api;
106pub(crate) mod modify;
107pub(crate) mod sketch;
108
109pub const MAX_SKETCH_CHECKPOINTS: usize = 100;
110
111#[derive(Debug, Clone)]
112struct SketchCheckpoint {
113 id: SketchCheckpointId,
114 source: SourceDelta,
115 program: Program,
116 scene_graph: SceneGraph,
117 exec_outcome: ExecOutcome,
118 point_freedom_cache: HashMap<ObjectId, Freedom>,
119 mock_memory: Option<SketchModeState>,
120}
121pub(crate) mod trim;
122
123struct ArcSizeConstraintParams {
124 points: Vec<ObjectId>,
125 function_name: &'static str,
126 value: f64,
127 units: NumericSuffix,
128 label_position: Option<Point2d<Number>>,
129 constraint_type_name: &'static str,
130}
131
132const POINT_FN: &str = "point";
133const POINT_AT_PARAM: &str = "at";
134const LINE_FN: &str = "line";
135const LINE_VARIABLE: &str = "line";
136const LINE_START_PARAM: &str = "start";
137const LINE_END_PARAM: &str = "end";
138const ARC_FN: &str = "arc";
139const ARC_VARIABLE: &str = "arc";
140const ARC_START_PARAM: &str = "start";
141const ARC_END_PARAM: &str = "end";
142const ARC_CENTER_PARAM: &str = "center";
143const ARC_DIRECTION_PARAM: &str = "direction";
144const ARC_DIRECTION_CW_NAME: &str = "CW";
146const CIRCLE_FN: &str = "circle";
147const CIRCLE_VARIABLE: &str = "circle";
148const CIRCLE_START_PARAM: &str = "start";
149const CIRCLE_CENTER_PARAM: &str = "center";
150const CONTROL_POINT_SPLINE_FN: &str = "controlPointSpline";
151const CONTROL_POINT_SPLINE_POINTS_PARAM: &str = "points";
152const LABEL_POSITION_PARAM: &str = "labelPosition";
153
154const COINCIDENT_FN: &str = "coincident";
155const DIAMETER_FN: &str = "diameter";
156const DISTANCE_FN: &str = "distance";
157const FIXED_FN: &str = "fixed";
158const ANGLE_FN: &str = "angle";
159const ANGLE_DIMENSION_FN: &str = "angleDimension";
160const ANGLE_LINES_PARAM: &str = "lines";
161const ANGLE_SECTOR_PARAM: &str = "sector";
162const ANGLE_INVERSE_PARAM: &str = "inverse";
163const HORIZONTAL_DISTANCE_FN: &str = "horizontalDistance";
164const VERTICAL_DISTANCE_FN: &str = "verticalDistance";
165const EQUAL_LENGTH_FN: &str = "equalLength";
166const EQUAL_RADIUS_FN: &str = "equalRadius";
167const HORIZONTAL_FN: &str = "horizontal";
168const MIDPOINT_FN: &str = "midpoint";
169const MIDPOINT_POINT_PARAM: &str = "point";
170const RADIUS_FN: &str = "radius";
171const SYMMETRIC_FN: &str = "symmetric";
172const SYMMETRIC_AXIS_PARAM: &str = "axis";
173const TANGENT_FN: &str = "tangent";
174const VERTICAL_FN: &str = "vertical";
175
176const LINE_PROPERTY_START: &str = "start";
177const LINE_PROPERTY_END: &str = "end";
178
179const ARC_PROPERTY_START: &str = "start";
180const ARC_PROPERTY_END: &str = "end";
181const ARC_PROPERTY_CENTER: &str = "center";
182const CIRCLE_PROPERTY_START: &str = "start";
183const CIRCLE_PROPERTY_CENTER: &str = "center";
184const CONTROL_POINT_SPLINE_PROPERTY_CONTROLS: &str = "controls";
185const CONTROL_POINT_SPLINE_PROPERTY_EDGES: &str = "edges";
186
187const CONSTRUCTION_PARAM: &str = "construction";
188
189#[derive(Debug, Clone, Copy)]
190enum EditDeleteKind {
191 Edit,
192 DeleteNonSketch,
193}
194
195struct ExecuteAfterEditOptions {
197 segment_ids_edited: AhashIndexSet<ObjectId>,
198 edit_kind: EditDeleteKind,
199 commit_solved_initial_guesses: bool,
200}
201
202impl EditDeleteKind {
203 fn is_delete(&self) -> bool {
205 match self {
206 EditDeleteKind::Edit => false,
207 EditDeleteKind::DeleteNonSketch => true,
208 }
209 }
210
211 fn to_change_kind(self) -> ChangeKind {
212 match self {
213 EditDeleteKind::Edit => ChangeKind::Edit,
214 EditDeleteKind::DeleteNonSketch => ChangeKind::Delete,
215 }
216 }
217}
218
219#[derive(Debug, Clone, Copy)]
220enum ChangeKind {
221 Add,
222 Edit,
223 Delete,
224 None,
225}
226
227#[derive(Debug, Clone, Serialize, ts_rs::TS)]
228#[ts(export, export_to = "FrontendApi.ts")]
229#[serde(tag = "type")]
230pub enum SetProgramOutcome {
231 #[serde(rename_all = "camelCase")]
232 Success {
233 scene_graph: Box<SceneGraph>,
234 exec_outcome: Box<ExecOutcome>,
235 checkpoint_id: Option<SketchCheckpointId>,
236 },
237 #[serde(rename_all = "camelCase")]
238 ExecFailure { error: Box<KclErrorWithOutputs> },
239}
240
241pub struct EditSegmentsOptions {
243 pub anchor_segment_ids: Option<Vec<ObjectId>>,
249 pub drag_anchors: Vec<SegmentDragAnchor>,
252 pub constraint_label_edits: Vec<ConstraintLabelPositionEdit>,
255 pub commit_solved_initial_guesses: bool,
257}
258
259pub struct EditDistanceConstraintLabelPositionOptions {
261 pub anchor_segment_ids: Vec<ObjectId>,
263 pub commit_solved_initial_guesses: bool,
265}
266
267pub struct EditConstraintOptions {
269 pub commit_solved_initial_guesses: bool,
271}
272
273#[derive(Debug, Clone)]
274struct SolidAstReference {
275 variable_name: String,
276 output_index: Option<usize>,
277}
278
279#[derive(Debug, Clone)]
280pub struct FrontendState {
281 program: Program,
282 scene_graph: SceneGraph,
283 solid_references: HashMap<Uuid, SolidAstReference>,
285 point_freedom_cache: HashMap<ObjectId, Freedom>,
288 next_drag_anchor_segment_ids: Option<AhashIndexSet<ObjectId>>,
291 next_segment_drag_anchors: Option<Vec<SegmentDragAnchor>>,
294 next_constraint_label_edits: Option<Vec<ConstraintLabelPositionEdit>>,
296 next_edit_commits_solver_solutions: Option<bool>,
300 sketch_checkpoints: VecDeque<SketchCheckpoint>,
301 sketch_checkpoint_id_gen: IncIdGenerator<u64>,
302}
303
304impl Default for FrontendState {
305 fn default() -> Self {
306 Self::new()
307 }
308}
309
310impl FrontendState {
311 pub fn new() -> Self {
312 Self {
313 program: Program::empty(),
314 scene_graph: SceneGraph {
315 project: ProjectId(0),
316 file: FileId(0),
317 version: Version(0),
318 objects: Default::default(),
319 settings: Default::default(),
320 sketch_mode: Default::default(),
321 },
322 solid_references: HashMap::new(),
323 point_freedom_cache: HashMap::new(),
324 next_drag_anchor_segment_ids: None,
325 next_segment_drag_anchors: None,
326 next_constraint_label_edits: None,
327 next_edit_commits_solver_solutions: None,
328 sketch_checkpoints: VecDeque::new(),
329 sketch_checkpoint_id_gen: IncIdGenerator::new(1),
330 }
331 }
332
333 pub fn scene_graph(&self) -> &SceneGraph {
335 &self.scene_graph
336 }
337
338 pub fn default_length_unit(&self) -> UnitLength {
339 self.program
340 .meta_settings()
341 .ok()
342 .flatten()
343 .map(|settings| settings.default_length_units)
344 .unwrap_or(UnitLength::Millimeters)
345 }
346
347 pub async fn create_sketch_checkpoint(
348 &mut self,
349 cache: &crate::Cache,
350 exec_outcome: ExecOutcome,
351 ) -> api::Result<SketchCheckpointId> {
352 let checkpoint_id = SketchCheckpointId::new(self.sketch_checkpoint_id_gen.next_id());
353
354 let checkpoint = SketchCheckpoint {
355 id: checkpoint_id,
356 source: SourceDelta {
357 text: source_from_ast(&self.program.ast),
358 },
359 program: self.program.clone(),
360 scene_graph: self.scene_graph.clone(),
361 exec_outcome,
362 point_freedom_cache: self.point_freedom_cache.clone(),
363 mock_memory: cache.read_old_memory().await,
364 };
365
366 self.sketch_checkpoints.push_back(checkpoint);
367 while self.sketch_checkpoints.len() > MAX_SKETCH_CHECKPOINTS {
368 self.sketch_checkpoints.pop_front();
369 }
370
371 Ok(checkpoint_id)
372 }
373
374 pub async fn edit_segments_with_options(
382 &mut self,
383 ctx: &ExecutorContext,
384 version: Version,
385 sketch: ObjectId,
386 segments: Vec<ExistingSegmentCtor>,
387 options: EditSegmentsOptions,
388 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
389 let previous_anchor_ids = options.anchor_segment_ids.map(|anchor_ids| {
390 self.next_drag_anchor_segment_ids
391 .replace(anchor_ids.into_iter().collect())
392 });
393 let previous_drag_anchors = self.next_segment_drag_anchors.replace(options.drag_anchors);
394 let previous_constraint_label_edits = self.next_constraint_label_edits.replace(options.constraint_label_edits);
395 let previous_commit_mode = self
396 .next_edit_commits_solver_solutions
397 .replace(options.commit_solved_initial_guesses);
398 let result = SketchApi::edit_segments(self, ctx, version, sketch, segments).await;
399 if let Some(previous_anchor_ids) = previous_anchor_ids {
400 self.next_drag_anchor_segment_ids = previous_anchor_ids;
401 }
402 self.next_segment_drag_anchors = previous_drag_anchors;
403 self.next_constraint_label_edits = previous_constraint_label_edits;
404 self.next_edit_commits_solver_solutions = previous_commit_mode;
405 result
406 }
407
408 pub async fn edit_distance_constraint_label_position_with_options(
414 &mut self,
415 ctx: &ExecutorContext,
416 version: Version,
417 sketch: ObjectId,
418 constraint_id: ObjectId,
419 label_position: Point2d<Number>,
420 options: EditDistanceConstraintLabelPositionOptions,
421 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
422 let previous_commit_mode = self
423 .next_edit_commits_solver_solutions
424 .replace(options.commit_solved_initial_guesses);
425 let result = SketchApi::edit_distance_constraint_label_position(
426 self,
427 ctx,
428 version,
429 sketch,
430 constraint_id,
431 label_position,
432 options.anchor_segment_ids,
433 )
434 .await;
435 self.next_edit_commits_solver_solutions = previous_commit_mode;
436 result
437 }
438
439 pub async fn edit_distance_constraint_with_options(
441 &mut self,
442 ctx: &ExecutorContext,
443 version: Version,
444 sketch: ObjectId,
445 constraint_id: ObjectId,
446 constraint: Constraint,
447 options: EditConstraintOptions,
448 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
449 self.edit_constraint_with_options(ctx, version, sketch, constraint_id, constraint, options)
450 .await
451 }
452
453 pub async fn edit_angle_constraint_with_options(
455 &mut self,
456 ctx: &ExecutorContext,
457 version: Version,
458 sketch: ObjectId,
459 constraint_id: ObjectId,
460 angle: Angle,
461 options: EditConstraintOptions,
462 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
463 self.edit_constraint_with_options(ctx, version, sketch, constraint_id, Constraint::Angle(angle), options)
464 .await
465 }
466
467 pub async fn edit_constraint_with_options(
469 &mut self,
470 ctx: &ExecutorContext,
471 _version: Version,
472 sketch: ObjectId,
473 constraint_id: ObjectId,
474 constraint: Constraint,
475 options: EditConstraintOptions,
476 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
477 let sketch_block_ref =
479 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
480
481 let object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
482 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Object not found: {constraint_id:?}")))
483 })?;
484
485 let mut new_ast = self.program.ast.clone();
486 let command = match &object.kind {
487 ObjectKind::Constraint {
488 constraint:
489 Constraint::Distance(_) | Constraint::HorizontalDistance(_) | Constraint::VerticalDistance(_),
490 } => {
491 let (function_name, distance) = match &constraint {
492 Constraint::Distance(distance) => (DISTANCE_FN, distance),
493 Constraint::HorizontalDistance(distance) => (HORIZONTAL_DISTANCE_FN, distance),
494 Constraint::VerticalDistance(distance) => (VERTICAL_DISTANCE_FN, distance),
495 _ => {
496 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
497 "A distance constraint can only be replaced by another distance constraint".to_owned(),
498 )));
499 }
500 };
501 let (call, value) = self
502 .distance_constraint_ast_parts(function_name, distance, &mut new_ast)
503 .map_err(KclErrorWithOutputs::no_outputs)?;
504 AstMutateCommand::EditDistanceConstraint { call, value }
505 }
506 ObjectKind::Constraint {
507 constraint: Constraint::Angle(_),
508 } => {
509 let Constraint::Angle(angle) = &constraint else {
510 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
511 "An angle constraint can only be replaced by another angle constraint".to_owned(),
512 )));
513 };
514 let (call, value) = self
515 .angle_constraint_ast_parts(angle, &mut new_ast)
516 .map_err(KclErrorWithOutputs::no_outputs)?;
517 AstMutateCommand::EditAngleConstraint { call, value }
518 }
519 ObjectKind::Constraint { .. } => {
520 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
521 "Editing {} is not supported",
522 object.kind.human_friendly_kind_with_article(),
523 ))));
524 }
525 _ => {
526 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
527 "Object is not a constraint: {constraint_id:?}"
528 ))));
529 }
530 };
531
532 self.mutate_ast(&mut new_ast, constraint_id, command)
533 .map_err(KclErrorWithOutputs::no_outputs)?;
534
535 self.execute_after_edit(
536 ctx,
537 sketch,
538 sketch_block_ref,
539 &mut new_ast,
540 ExecuteAfterEditOptions {
541 segment_ids_edited: Default::default(),
542 edit_kind: EditDeleteKind::Edit,
543 commit_solved_initial_guesses: options.commit_solved_initial_guesses,
544 },
545 )
546 .await
547 }
548
549 pub async fn restore_sketch_checkpoint(
550 &mut self,
551 cache: &crate::Cache,
552 checkpoint_id: SketchCheckpointId,
553 ) -> api::Result<RestoreSketchCheckpointOutcome> {
554 let checkpoint = self
555 .sketch_checkpoints
556 .iter()
557 .find(|checkpoint| checkpoint.id == checkpoint_id)
558 .cloned()
559 .ok_or_else(|| Error {
560 msg: format!("Sketch checkpoint not found: {checkpoint_id:?}"),
561 })?;
562
563 self.program = checkpoint.program;
564 self.scene_graph = checkpoint.scene_graph.clone();
565 self.solid_references = solid_references_from_variables(&self.program.ast, &checkpoint.exec_outcome.variables);
566 self.point_freedom_cache = checkpoint.point_freedom_cache;
567 self.next_drag_anchor_segment_ids = None;
568 self.next_segment_drag_anchors = None;
569 self.next_constraint_label_edits = None;
570 self.next_edit_commits_solver_solutions = None;
571
572 if let Some(mock_memory) = checkpoint.mock_memory {
573 cache.write_old_memory(mock_memory).await;
574 } else {
575 cache.clear_mem_cache().await;
576 }
577
578 Ok(RestoreSketchCheckpointOutcome {
579 source_delta: checkpoint.source,
580 scene_graph_delta: SceneGraphDelta {
581 new_graph: self.scene_graph_for_ui(),
582 new_objects: Vec::new(),
583 invalidates_ids: true,
584 exec_outcome: ExecOutcome {
587 engine_scene_generation: cache.engine_scene_generation().await,
588 ..checkpoint.exec_outcome
589 },
590 },
591 })
592 }
593
594 pub fn clear_sketch_checkpoints(&mut self) {
595 self.sketch_checkpoints.clear();
596 }
597 fn scene_graph_for_ui(&self) -> SceneGraph {
598 let has_control_point_splines = self.scene_graph.objects.iter().any(|object| {
599 matches!(
600 object.kind,
601 ObjectKind::Segment {
602 segment: Segment::ControlPointSpline(_)
603 }
604 )
605 });
606
607 if !has_control_point_splines {
608 return self.scene_graph.clone();
609 }
610
611 let hidden_constraint_ids = self
612 .scene_graph
613 .objects
614 .iter()
615 .filter_map(|object| match &object.kind {
616 ObjectKind::Constraint {
617 constraint: Constraint::Coincident(coincident),
618 } if coincident_is_internal_to_same_control_point_spline(coincident, &self.scene_graph) => {
619 Some(object.id)
620 }
621 _ => None,
622 })
623 .collect::<HashSet<_>>();
624
625 if hidden_constraint_ids.is_empty() {
626 return self.scene_graph.clone();
627 }
628
629 let mut scene_graph = self.scene_graph.clone();
630 for object in &mut scene_graph.objects {
631 match &mut object.kind {
632 ObjectKind::Constraint { .. } if hidden_constraint_ids.contains(&object.id) => {
633 object.kind = ObjectKind::Nil;
634 }
635 ObjectKind::Sketch(sketch) => {
636 sketch
637 .constraints
638 .retain(|constraint_id| !hidden_constraint_ids.contains(constraint_id));
639 }
640 _ => {}
641 }
642 }
643
644 scene_graph
645 }
646}
647
648fn coincident_is_internal_to_same_control_point_spline(coincident: &Coincident, scene_graph: &SceneGraph) -> bool {
649 let mut first_owner_id = None;
650 for segment_id in coincident.segment_ids() {
651 let Some(owner_id) = owning_control_point_spline_id(segment_id, scene_graph) else {
652 return false;
653 };
654
655 match first_owner_id {
656 Some(first_owner_id) if first_owner_id != owner_id => return false,
657 Some(_) => {}
658 None => first_owner_id = Some(owner_id),
659 }
660 }
661
662 first_owner_id.is_some()
663}
664
665fn owning_control_point_spline_id(segment_id: ObjectId, scene_graph: &SceneGraph) -> Option<ObjectId> {
666 let object = scene_graph.objects.get(segment_id.0)?;
667 let ObjectKind::Segment { segment } = &object.kind else {
668 return None;
669 };
670
671 match segment {
672 Segment::ControlPointSpline(_) => Some(segment_id),
673 Segment::Point(point) => point
674 .owner
675 .filter(|owner_id| matches_control_point_spline_owner(*owner_id, scene_graph)),
676 Segment::Line(line) => line
677 .owner
678 .filter(|owner_id| matches_control_point_spline_owner(*owner_id, scene_graph)),
679 _ => None,
680 }
681}
682
683fn matches_control_point_spline_owner(owner_id: ObjectId, scene_graph: &SceneGraph) -> bool {
684 matches!(
685 scene_graph.objects.get(owner_id.0).map(|object| &object.kind),
686 Some(ObjectKind::Segment {
687 segment: Segment::ControlPointSpline(_)
688 })
689 )
690}
691
692fn ensure_control_point_spline_experimental_features(program: &Program) -> Result<Program, KclError> {
693 let experimental_features_allowed = program
694 .meta_settings()
695 .ok()
696 .flatten()
697 .map(|settings| settings.experimental_features == WarningLevel::Allow)
698 .unwrap_or(false);
699 if experimental_features_allowed {
700 return Ok(program.clone());
701 }
702
703 program.change_experimental_features(Some(WarningLevel::Allow))
704}
705
706impl SketchApi for FrontendState {
707 async fn execute_mock(
708 &mut self,
709 ctx: &ExecutorContext,
710 _version: Version,
711 sketch: ObjectId,
712 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
713 let sketch_block_ref =
714 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
715
716 let mut truncated_program = self.program.clone();
717 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::None)
718 .map_err(KclErrorWithOutputs::no_outputs)?;
719
720 let outcome = ctx
722 .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch))
723 .await?;
724 let new_source = source_from_ast(&self.program.ast);
725 let src_delta = SourceDelta { text: new_source };
726 let outcome = self.update_state_after_exec(outcome, true);
728 let scene_graph_delta = SceneGraphDelta {
729 new_graph: self.scene_graph.clone(),
730 new_objects: Default::default(),
731 invalidates_ids: false,
732 exec_outcome: outcome,
733 };
734 Ok((src_delta, scene_graph_delta))
735 }
736
737 async fn new_sketch(
738 &mut self,
739 ctx: &ExecutorContext,
740 _project: ProjectId,
741 _file: FileId,
742 _version: Version,
743 args: SketchCtor,
744 ) -> ExecResult<(SourceDelta, SceneGraphDelta, ObjectId)> {
745 let mut new_ast = self.program.ast.clone();
748 let mut plane_ast = sketch_on_ast_expr(&mut new_ast, &self.scene_graph, &self.solid_references, &args.on)
750 .map_err(KclErrorWithOutputs::no_outputs)?;
751 let mut defined_names = find_defined_names(&new_ast);
752 let is_face_of_expr = matches!(
753 &plane_ast,
754 ast::Expr::CallExpressionKw(call) if call.callee.name.name == "faceOf"
755 );
756 if is_face_of_expr {
757 let face_name = next_free_name_with_padding("face", &defined_names)
758 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
759 let face_decl = ast::VariableDeclaration::new(
760 ast::VariableDeclarator::new(&face_name, plane_ast),
761 ast::ItemVisibility::Default,
762 ast::VariableKind::Const,
763 );
764 new_ast
765 .body
766 .push(ast::BodyItem::VariableDeclaration(BoxNode::new(ast::Node::no_src(
767 face_decl,
768 ))));
769 defined_names.insert(face_name.clone());
770 plane_ast = ast::Expr::Name(BoxNode::new(ast::Name::new(&face_name)));
771 }
772 let sketch_ast = ast::SketchBlock {
773 arguments: vec![ast::LabeledArg {
774 label: Some(ast::Identifier::new(SKETCH_BLOCK_PARAM_ON)),
775 arg: plane_ast,
776 }],
777 body: Default::default(),
778 is_being_edited: false,
779 non_code_meta: Default::default(),
780 digest: None,
781 };
782 let sketch_name = next_free_name_with_padding("sketch", &defined_names)
785 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
786 let sketch_decl = ast::VariableDeclaration::new(
787 ast::VariableDeclarator::new(
788 &sketch_name,
789 ast::Expr::SketchBlock(BoxNode::new(ast::Node::no_src(sketch_ast))),
790 ),
791 ast::ItemVisibility::Default,
792 ast::VariableKind::Const,
793 );
794 new_ast
795 .body
796 .push(ast::BodyItem::VariableDeclaration(BoxNode::new(ast::Node::no_src(
797 sketch_decl,
798 ))));
799 let new_source = source_from_ast(&new_ast);
801 let new_program = parse_frontend_mutation_source(
803 &new_source,
804 "Error parsing KCL source after adding sketch",
805 "No AST produced after adding sketch",
806 )?;
807
808 self.program = new_program.clone();
810
811 let outcome = ctx.run_with_caching(new_program.clone()).await?;
814 let freedom_analysis_ran = true;
815
816 let outcome = self.update_state_after_exec(outcome, freedom_analysis_ran);
817
818 let Some(sketch_id) = self
819 .scene_graph
820 .objects
821 .iter()
822 .filter_map(|object| match object.kind {
823 ObjectKind::Sketch(_) => Some(object.id),
824 _ => None,
825 })
826 .max_by_key(|id| id.0)
827 else {
828 return Err(KclErrorWithOutputs::from_error_outcome(
829 KclError::refactor("No objects in scene graph after adding sketch".to_owned()),
830 outcome,
831 ));
832 };
833 self.scene_graph.sketch_mode = Some(sketch_id);
835
836 let src_delta = SourceDelta { text: new_source };
837 let scene_graph_delta = SceneGraphDelta {
838 new_graph: self.scene_graph_for_ui(),
839 invalidates_ids: false,
840 new_objects: vec![sketch_id],
841 exec_outcome: outcome,
842 };
843 Ok((src_delta, scene_graph_delta, sketch_id))
844 }
845
846 async fn edit_sketch(
847 &mut self,
848 ctx: &ExecutorContext,
849 _project: ProjectId,
850 _file: FileId,
851 _version: Version,
852 sketch: ObjectId,
853 ) -> ExecResult<SceneGraphDelta> {
854 let sketch_object = self.scene_graph.objects.get(sketch.0).ok_or_else(|| {
858 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
859 })?;
860 let ObjectKind::Sketch(_) = &sketch_object.kind else {
861 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
862 "Object is not a sketch, it is {}",
863 sketch_object.kind.human_friendly_kind_with_article()
864 ))));
865 };
866 let sketch_block_ref = expect_single_node_ref(sketch_object).map_err(KclErrorWithOutputs::no_outputs)?;
867
868 self.scene_graph.sketch_mode = Some(sketch);
870
871 let mut truncated_program = self.program.clone();
873 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::None)
874 .map_err(KclErrorWithOutputs::no_outputs)?;
875
876 let outcome = ctx
879 .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch))
880 .await?;
881
882 let outcome = self.update_state_after_exec(outcome, true);
884 let scene_graph_delta = SceneGraphDelta {
885 new_graph: self.scene_graph_for_ui(),
886 invalidates_ids: false,
887 new_objects: Vec::new(),
888 exec_outcome: outcome,
889 };
890 Ok(scene_graph_delta)
891 }
892
893 async fn exit_sketch(
894 &mut self,
895 ctx: &ExecutorContext,
896 _version: Version,
897 sketch: ObjectId,
898 ) -> ExecResult<SceneGraph> {
899 #[cfg(not(target_arch = "wasm32"))]
901 let _ = sketch;
902 #[cfg(target_arch = "wasm32")]
903 if self.scene_graph.sketch_mode != Some(sketch) {
904 web_sys::console::warn_1(
905 &format!(
906 "WARNING: exit_sketch: current state's sketch mode ID doesn't match the given sketch ID; state={:#?}, given={sketch:?}",
907 self.scene_graph.sketch_mode
908 )
909 .into(),
910 );
911 }
912 self.scene_graph.sketch_mode = None;
913
914 let outcome = ctx.run_with_caching(self.program.clone()).await?;
916
917 self.update_state_after_exec(outcome, false);
919
920 Ok(self.scene_graph_for_ui())
921 }
922
923 async fn delete_sketch(
924 &mut self,
925 ctx: &ExecutorContext,
926 _version: Version,
927 sketch: ObjectId,
928 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
929 let mut new_ast = self.program.ast.clone();
932
933 let sketch_id = sketch;
935 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
936 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
937 })?;
938 let ObjectKind::Sketch(_) = &sketch_object.kind else {
939 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
940 "Object is not a sketch, it is {}",
941 sketch_object.kind.human_friendly_kind_with_article(),
942 ))));
943 };
944
945 self.mutate_ast(&mut new_ast, sketch_id, AstMutateCommand::DeleteNode)
947 .map_err(KclErrorWithOutputs::no_outputs)?;
948
949 self.execute_after_delete_sketch(ctx, &mut new_ast).await
950 }
951
952 async fn add_segment(
953 &mut self,
954 ctx: &ExecutorContext,
955 _version: Version,
956 sketch: ObjectId,
957 segment: SegmentCtor,
958 _label: Option<String>,
959 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
960 match segment {
962 SegmentCtor::Point(ctor) => self.add_point(ctx, sketch, ctor).await,
963 SegmentCtor::Line(ctor) => self.add_line(ctx, sketch, ctor).await,
964 SegmentCtor::Arc(ctor) => self.add_arc(ctx, sketch, ctor).await,
965 SegmentCtor::Circle(ctor) => self.add_circle(ctx, sketch, ctor).await,
966 SegmentCtor::ControlPointSpline(ctor) => self.add_control_point_spline(ctx, sketch, ctor).await,
967 }
968 }
969
970 async fn edit_segments(
971 &mut self,
972 ctx: &ExecutorContext,
973 _version: Version,
974 sketch: ObjectId,
975 segments: Vec<ExistingSegmentCtor>,
976 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
977 let sketch_block_ref =
979 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
980
981 let mut new_ast = self.program.ast.clone();
982 let mut edited_segment_ids = AhashIndexSet::with_capacity_and_hasher(segments.len(), Default::default());
983 let mut invalidates_ids = false;
984
985 for segment in &segments {
988 edited_segment_ids.insert(segment.id);
989 if let SegmentCtor::ControlPointSpline(new_ctor) = &segment.ctor
990 && let Some(existing_object) = self.scene_graph.objects.get(segment.id.0)
991 && let ObjectKind::Segment {
992 segment: Segment::ControlPointSpline(existing_spline),
993 } = &existing_object.kind
994 && existing_spline.controls.len() != new_ctor.points.len()
995 {
996 invalidates_ids = true;
997 }
998 }
999 let drag_anchor_segment_ids = self
1000 .next_drag_anchor_segment_ids
1001 .take()
1002 .unwrap_or_else(|| edited_segment_ids.clone());
1003 let constraint_label_edits = self.next_constraint_label_edits.take().unwrap_or_default();
1004 let commit_solved_initial_guesses = self.next_edit_commits_solver_solutions.take().unwrap_or(true);
1005
1006 let mut final_edits: IndexMap<ObjectId, SegmentCtor> = IndexMap::new();
1021
1022 for segment in segments {
1023 let segment_id = segment.id;
1024 match segment.ctor {
1025 SegmentCtor::Point(ctor) => {
1026 if let Some(segment_object) = self.scene_graph.objects.get(segment_id.0)
1028 && let ObjectKind::Segment { segment } = &segment_object.kind
1029 && let Segment::Point(point) = segment
1030 && let Some(owner_id) = point.owner
1031 && let Some(owner_object) = self.scene_graph.objects.get(owner_id.0)
1032 && let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind
1033 {
1034 match owner_segment {
1035 Segment::Line(line) if line.start == segment_id || line.end == segment_id => {
1036 if let Some(existing) = final_edits.get_mut(&owner_id) {
1037 let SegmentCtor::Line(line_ctor) = existing else {
1038 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1039 "Internal: Expected line ctor for owner, but found {}",
1040 existing.human_friendly_kind_with_article()
1041 ))));
1042 };
1043 if line.start == segment_id {
1045 line_ctor.start = ctor.position;
1046 } else {
1047 line_ctor.end = ctor.position;
1048 }
1049 } else if let SegmentCtor::Line(line_ctor) = &line.ctor {
1050 let mut line_ctor = line_ctor.clone();
1052 if line.start == segment_id {
1053 line_ctor.start = ctor.position;
1054 } else {
1055 line_ctor.end = ctor.position;
1056 }
1057 final_edits.insert(owner_id, SegmentCtor::Line(line_ctor));
1058 } else {
1059 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1061 "Internal: Line does not have line ctor, but found {}",
1062 line.ctor.human_friendly_kind_with_article()
1063 ))));
1064 }
1065 continue;
1066 }
1067 Segment::Arc(arc)
1068 if arc.start == segment_id || arc.end == segment_id || arc.center == segment_id =>
1069 {
1070 if let Some(existing) = final_edits.get_mut(&owner_id) {
1071 let SegmentCtor::Arc(arc_ctor) = existing else {
1072 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1073 "Internal: Expected arc ctor for owner, but found {}",
1074 existing.human_friendly_kind_with_article()
1075 ))));
1076 };
1077 if arc.start == segment_id {
1078 arc_ctor.start = ctor.position;
1079 } else if arc.end == segment_id {
1080 arc_ctor.end = ctor.position;
1081 } else {
1082 arc_ctor.center = ctor.position;
1083 }
1084 } else if let SegmentCtor::Arc(arc_ctor) = &arc.ctor {
1085 let mut arc_ctor = arc_ctor.clone();
1086 if arc.start == segment_id {
1087 arc_ctor.start = ctor.position;
1088 } else if arc.end == segment_id {
1089 arc_ctor.end = ctor.position;
1090 } else {
1091 arc_ctor.center = ctor.position;
1092 }
1093 final_edits.insert(owner_id, SegmentCtor::Arc(arc_ctor));
1094 } else {
1095 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1096 "Internal: Arc does not have arc ctor, but found {}",
1097 arc.ctor.human_friendly_kind_with_article()
1098 ))));
1099 }
1100 continue;
1101 }
1102 Segment::Circle(circle) if circle.start == segment_id || circle.center == segment_id => {
1103 if let Some(existing) = final_edits.get_mut(&owner_id) {
1104 let SegmentCtor::Circle(circle_ctor) = existing else {
1105 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1106 "Internal: Expected circle ctor for owner, but found {}",
1107 existing.human_friendly_kind_with_article()
1108 ))));
1109 };
1110 if circle.start == segment_id {
1111 circle_ctor.start = ctor.position;
1112 } else {
1113 circle_ctor.center = ctor.position;
1114 }
1115 } else if let SegmentCtor::Circle(circle_ctor) = &circle.ctor {
1116 let mut circle_ctor = circle_ctor.clone();
1117 if circle.start == segment_id {
1118 circle_ctor.start = ctor.position;
1119 } else {
1120 circle_ctor.center = ctor.position;
1121 }
1122 final_edits.insert(owner_id, SegmentCtor::Circle(circle_ctor));
1123 } else {
1124 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1125 "Internal: Circle does not have circle ctor, but found {}",
1126 circle.ctor.human_friendly_kind_with_article()
1127 ))));
1128 }
1129 continue;
1130 }
1131 Segment::ControlPointSpline(spline) if spline.controls.contains(&segment_id) => {
1132 let Some(control_index) =
1133 spline.controls.iter().position(|control_id| *control_id == segment_id)
1134 else {
1135 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1136 "Internal: Point is not part of owner's controlPointSpline segment: point={segment_id:?}, spline={owner_id:?}"
1137 ))));
1138 };
1139 if let Some(existing) = final_edits.get_mut(&owner_id) {
1140 let SegmentCtor::ControlPointSpline(spline_ctor) = existing else {
1141 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1142 "Internal: Expected controlPointSpline ctor for owner, but found {}",
1143 existing.human_friendly_kind_with_article()
1144 ))));
1145 };
1146 spline_ctor.points[control_index] = ctor.position;
1147 } else if let SegmentCtor::ControlPointSpline(spline_ctor) = &spline.ctor {
1148 let mut spline_ctor = spline_ctor.clone();
1149 spline_ctor.points[control_index] = ctor.position;
1150 final_edits.insert(owner_id, SegmentCtor::ControlPointSpline(spline_ctor));
1151 } else {
1152 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1153 "Internal: Control point spline does not have controlPointSpline ctor, but found {}",
1154 spline.ctor.human_friendly_kind_with_article()
1155 ))));
1156 }
1157 continue;
1158 }
1159 _ => {}
1160 }
1161 }
1162
1163 final_edits.insert(segment_id, SegmentCtor::Point(ctor));
1165 }
1166 SegmentCtor::Line(ctor) => {
1167 final_edits.insert(segment_id, SegmentCtor::Line(ctor));
1168 }
1169 SegmentCtor::Arc(ctor) => {
1170 final_edits.insert(segment_id, SegmentCtor::Arc(ctor));
1171 }
1172 SegmentCtor::Circle(ctor) => {
1173 final_edits.insert(segment_id, SegmentCtor::Circle(ctor));
1174 }
1175 SegmentCtor::ControlPointSpline(ctor) => {
1176 final_edits.insert(segment_id, SegmentCtor::ControlPointSpline(ctor));
1177 }
1178 }
1179 }
1180
1181 for (segment_id, ctor) in final_edits {
1182 match ctor {
1183 SegmentCtor::Point(ctor) => self
1184 .edit_point(&mut new_ast, sketch, segment_id, ctor)
1185 .map_err(KclErrorWithOutputs::no_outputs)?,
1186 SegmentCtor::Line(ctor) => self
1187 .edit_line(&mut new_ast, sketch, segment_id, ctor)
1188 .map_err(KclErrorWithOutputs::no_outputs)?,
1189 SegmentCtor::Arc(ctor) => self
1190 .edit_arc(&mut new_ast, sketch, segment_id, ctor)
1191 .map_err(KclErrorWithOutputs::no_outputs)?,
1192 SegmentCtor::Circle(ctor) => self
1193 .edit_circle(&mut new_ast, sketch, segment_id, ctor)
1194 .map_err(KclErrorWithOutputs::no_outputs)?,
1195 SegmentCtor::ControlPointSpline(ctor) => self
1196 .edit_control_point_spline(&mut new_ast, sketch, segment_id, ctor)
1197 .map_err(KclErrorWithOutputs::no_outputs)?,
1198 }
1199 }
1200 for edit in constraint_label_edits {
1201 self.mutate_constraint_label_position(&mut new_ast, edit.constraint_id, edit.label_position)
1202 .map_err(KclErrorWithOutputs::no_outputs)?;
1203 }
1204 let (source_delta, mut scene_graph_delta) = self
1205 .execute_after_edit(
1206 ctx,
1207 sketch,
1208 sketch_block_ref,
1209 &mut new_ast,
1210 ExecuteAfterEditOptions {
1211 segment_ids_edited: drag_anchor_segment_ids,
1212 edit_kind: EditDeleteKind::Edit,
1213 commit_solved_initial_guesses,
1214 },
1215 )
1216 .await?;
1217 if invalidates_ids {
1218 scene_graph_delta.invalidates_ids = true;
1219 }
1220 Ok((source_delta, scene_graph_delta))
1221 }
1222
1223 async fn delete_objects(
1224 &mut self,
1225 ctx: &ExecutorContext,
1226 _version: Version,
1227 sketch: ObjectId,
1228 constraint_ids: Vec<ObjectId>,
1229 segment_ids: Vec<ObjectId>,
1230 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1231 let sketch_block_ref =
1233 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1234
1235 let mut constraint_ids_set = constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
1237 let segment_ids_set = segment_ids.into_iter().collect::<AhashIndexSet<_>>();
1238
1239 let mut resolved_segment_ids_to_delete = AhashIndexSet::default();
1242
1243 for segment_id in segment_ids_set.iter().copied() {
1244 let owner_id = self.scene_graph.objects.get(segment_id.0).and_then(|segment_object| {
1245 let ObjectKind::Segment { segment } = &segment_object.kind else {
1246 return None;
1247 };
1248 match segment {
1249 Segment::Point(point) => point.owner,
1250 Segment::Line(line) => line.owner,
1251 _ => None,
1252 }
1253 });
1254
1255 if let Some(owner_id) = owner_id
1256 && let Some(owner_object) = self.scene_graph.objects.get(owner_id.0)
1257 && let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind
1258 && matches!(
1259 owner_segment,
1260 Segment::Line(_) | Segment::Arc(_) | Segment::Circle(_) | Segment::ControlPointSpline(_)
1261 )
1262 {
1263 resolved_segment_ids_to_delete.insert(owner_id);
1265 } else {
1266 resolved_segment_ids_to_delete.insert(segment_id);
1268 }
1269 }
1270 let referenced_constraint_ids = self
1271 .find_referenced_constraints(sketch, &resolved_segment_ids_to_delete)
1272 .map_err(KclErrorWithOutputs::no_outputs)?;
1273
1274 let mut new_ast = self.program.ast.clone();
1275
1276 for constraint_id in referenced_constraint_ids {
1277 if constraint_ids_set.contains(&constraint_id) {
1278 continue;
1279 }
1280
1281 let constraint_object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
1282 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Constraint not found: {constraint_id:?}")))
1283 })?;
1284 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
1285 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1286 "Object is not a constraint, it is {}",
1287 constraint_object.kind.human_friendly_kind_with_article()
1288 ))));
1289 };
1290
1291 match constraint {
1292 Constraint::Coincident(coincident) => {
1293 let remaining_segments =
1294 self.remaining_constraint_segments(&coincident.segments, &resolved_segment_ids_to_delete);
1295
1296 if remaining_segments.len() >= 2 {
1298 self.edit_coincident_constraint(&mut new_ast, constraint_id, remaining_segments)
1299 .map_err(KclErrorWithOutputs::no_outputs)?;
1300 } else {
1301 constraint_ids_set.insert(constraint_id);
1302 }
1303 }
1304 Constraint::EqualRadius(equal_radius) => {
1305 let remaining_input = equal_radius
1306 .input
1307 .iter()
1308 .copied()
1309 .filter(|segment_id| {
1310 !self.segment_will_be_deleted(*segment_id, &resolved_segment_ids_to_delete)
1311 })
1312 .collect::<Vec<_>>();
1313
1314 if remaining_input.len() >= 2 {
1315 self.edit_equal_radius_constraint(&mut new_ast, constraint_id, remaining_input)
1316 .map_err(KclErrorWithOutputs::no_outputs)?;
1317 } else {
1318 constraint_ids_set.insert(constraint_id);
1319 }
1320 }
1321 Constraint::LinesEqualLength(lines_equal_length) => {
1322 let remaining_lines = lines_equal_length
1323 .lines
1324 .iter()
1325 .copied()
1326 .filter(|line_id| !self.segment_will_be_deleted(*line_id, &resolved_segment_ids_to_delete))
1327 .collect::<Vec<_>>();
1328
1329 if remaining_lines.len() >= 2 {
1331 self.edit_equal_length_constraint(&mut new_ast, constraint_id, remaining_lines)
1332 .map_err(KclErrorWithOutputs::no_outputs)?;
1333 } else {
1334 constraint_ids_set.insert(constraint_id);
1335 }
1336 }
1337 Constraint::Parallel(parallel) => {
1338 let remaining_lines = parallel
1339 .lines
1340 .iter()
1341 .copied()
1342 .filter(|line_id| !self.segment_will_be_deleted(*line_id, &resolved_segment_ids_to_delete))
1343 .collect::<Vec<_>>();
1344
1345 if remaining_lines.len() >= 2 {
1346 self.edit_parallel_constraint(&mut new_ast, constraint_id, remaining_lines)
1347 .map_err(KclErrorWithOutputs::no_outputs)?;
1348 } else {
1349 constraint_ids_set.insert(constraint_id);
1350 }
1351 }
1352 Constraint::Horizontal(Horizontal::Points { points }) => {
1353 let remaining_points = self.remaining_constraint_segments(points, &resolved_segment_ids_to_delete);
1354
1355 if remaining_points.len() >= 2 {
1356 self.edit_horizontal_points_constraint(&mut new_ast, constraint_id, remaining_points)
1357 .map_err(KclErrorWithOutputs::no_outputs)?;
1358 } else {
1359 constraint_ids_set.insert(constraint_id);
1360 }
1361 }
1362 Constraint::Vertical(Vertical::Points { points }) => {
1363 let remaining_points = self.remaining_constraint_segments(points, &resolved_segment_ids_to_delete);
1364
1365 if remaining_points.len() >= 2 {
1366 self.edit_vertical_points_constraint(&mut new_ast, constraint_id, remaining_points)
1367 .map_err(KclErrorWithOutputs::no_outputs)?;
1368 } else {
1369 constraint_ids_set.insert(constraint_id);
1370 }
1371 }
1372 Constraint::Fixed(fixed) => {
1373 if fixed.points.iter().any(|fixed_point| {
1374 self.segment_will_be_deleted(fixed_point.point, &resolved_segment_ids_to_delete)
1375 }) {
1376 constraint_ids_set.insert(constraint_id);
1377 }
1378 }
1379 _ => {
1380 constraint_ids_set.insert(constraint_id);
1382 }
1383 }
1384 }
1385
1386 for constraint_id in constraint_ids_set {
1387 self.delete_constraint(&mut new_ast, sketch, constraint_id)
1388 .map_err(KclErrorWithOutputs::no_outputs)?;
1389 }
1390 for segment_id in resolved_segment_ids_to_delete {
1391 self.delete_segment(&mut new_ast, sketch, segment_id)
1392 .map_err(KclErrorWithOutputs::no_outputs)?;
1393 }
1394
1395 self.execute_after_edit(
1396 ctx,
1397 sketch,
1398 sketch_block_ref,
1399 &mut new_ast,
1400 ExecuteAfterEditOptions {
1401 segment_ids_edited: Default::default(),
1402 edit_kind: EditDeleteKind::DeleteNonSketch,
1403 commit_solved_initial_guesses: true,
1404 },
1405 )
1406 .await
1407 }
1408
1409 async fn add_constraint(
1410 &mut self,
1411 ctx: &ExecutorContext,
1412 _version: Version,
1413 sketch: ObjectId,
1414 constraint: Constraint,
1415 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1416 let original_program = self.program.clone();
1420 let original_scene_graph = self.scene_graph.clone();
1421
1422 let mut new_ast = self.program.ast.clone();
1423 let sketch_block_ref = match constraint {
1424 Constraint::Coincident(coincident) => self
1425 .add_coincident(sketch, coincident, &mut new_ast)
1426 .await
1427 .map_err(KclErrorWithOutputs::no_outputs)?,
1428 Constraint::Distance(distance) => self
1429 .add_distance(sketch, distance, &mut new_ast)
1430 .await
1431 .map_err(KclErrorWithOutputs::no_outputs)?,
1432 Constraint::EqualRadius(equal_radius) => self
1433 .add_equal_radius(sketch, equal_radius, &mut new_ast)
1434 .await
1435 .map_err(KclErrorWithOutputs::no_outputs)?,
1436 Constraint::Fixed(fixed) => self
1437 .add_fixed_constraints(sketch, fixed.points, &mut new_ast)
1438 .await
1439 .map_err(KclErrorWithOutputs::no_outputs)?,
1440 Constraint::HorizontalDistance(distance) => self
1441 .add_horizontal_distance(sketch, distance, &mut new_ast)
1442 .await
1443 .map_err(KclErrorWithOutputs::no_outputs)?,
1444 Constraint::VerticalDistance(distance) => self
1445 .add_vertical_distance(sketch, distance, &mut new_ast)
1446 .await
1447 .map_err(KclErrorWithOutputs::no_outputs)?,
1448 Constraint::Horizontal(horizontal) => self
1449 .add_horizontal(sketch, horizontal, &mut new_ast)
1450 .await
1451 .map_err(KclErrorWithOutputs::no_outputs)?,
1452 Constraint::LinesEqualLength(lines_equal_length) => self
1453 .add_lines_equal_length(sketch, lines_equal_length, &mut new_ast)
1454 .await
1455 .map_err(KclErrorWithOutputs::no_outputs)?,
1456 Constraint::Midpoint(midpoint) => self
1457 .add_midpoint(sketch, midpoint, &mut new_ast)
1458 .await
1459 .map_err(KclErrorWithOutputs::no_outputs)?,
1460 Constraint::Parallel(parallel) => self
1461 .add_parallel(sketch, parallel, &mut new_ast)
1462 .await
1463 .map_err(KclErrorWithOutputs::no_outputs)?,
1464 Constraint::Perpendicular(perpendicular) => self
1465 .add_perpendicular(sketch, perpendicular, &mut new_ast)
1466 .await
1467 .map_err(KclErrorWithOutputs::no_outputs)?,
1468 Constraint::Radius(radius) => self
1469 .add_radius(sketch, radius, &mut new_ast)
1470 .await
1471 .map_err(KclErrorWithOutputs::no_outputs)?,
1472 Constraint::Diameter(diameter) => self
1473 .add_diameter(sketch, diameter, &mut new_ast)
1474 .await
1475 .map_err(KclErrorWithOutputs::no_outputs)?,
1476 Constraint::Symmetric(symmetric) => self
1477 .add_symmetric(sketch, symmetric, &mut new_ast)
1478 .await
1479 .map_err(KclErrorWithOutputs::no_outputs)?,
1480 Constraint::Vertical(vertical) => self
1481 .add_vertical(sketch, vertical, &mut new_ast)
1482 .await
1483 .map_err(KclErrorWithOutputs::no_outputs)?,
1484 Constraint::Angle(lines_at_angle) => self
1485 .add_angle(sketch, lines_at_angle, &mut new_ast)
1486 .await
1487 .map_err(KclErrorWithOutputs::no_outputs)?,
1488 Constraint::Tangent(tangent) => self
1489 .add_tangent(sketch, tangent, &mut new_ast)
1490 .await
1491 .map_err(KclErrorWithOutputs::no_outputs)?,
1492 };
1493
1494 let result = self
1495 .execute_after_add_constraint(ctx, sketch, sketch_block_ref, &mut new_ast)
1496 .await;
1497
1498 if result.is_err() {
1500 self.program = original_program;
1501 self.scene_graph = original_scene_graph;
1502 }
1503
1504 result
1505 }
1506
1507 async fn chain_segment(
1508 &mut self,
1509 ctx: &ExecutorContext,
1510 version: Version,
1511 sketch: ObjectId,
1512 previous_segment_end_point_id: ObjectId,
1513 segment: SegmentCtor,
1514 _label: Option<String>,
1515 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1516 let SegmentCtor::Line(line_ctor) = segment else {
1520 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1521 "chain_segment currently only supports Line segments, got {}",
1522 segment.human_friendly_kind_with_article(),
1523 ))));
1524 };
1525
1526 let (_first_src_delta, first_scene_delta) = self.add_line(ctx, sketch, line_ctor).await?;
1528
1529 let new_line_id = first_scene_delta
1532 .new_objects
1533 .iter()
1534 .find(|&obj_id| {
1535 let obj = self.scene_graph.objects.get(obj_id.0);
1536 if let Some(obj) = obj {
1537 matches!(
1538 &obj.kind,
1539 ObjectKind::Segment {
1540 segment: Segment::Line(_)
1541 }
1542 )
1543 } else {
1544 false
1545 }
1546 })
1547 .ok_or_else(|| {
1548 KclErrorWithOutputs::no_outputs(KclError::refactor(
1549 "Failed to find new line segment in scene graph".to_string(),
1550 ))
1551 })?;
1552
1553 let new_line_obj = self.scene_graph.objects.get(new_line_id.0).ok_or_else(|| {
1554 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1555 "New line object not found: {new_line_id:?}"
1556 )))
1557 })?;
1558
1559 let ObjectKind::Segment {
1560 segment: new_line_segment,
1561 } = &new_line_obj.kind
1562 else {
1563 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1564 "Object is not a segment: {new_line_obj:?}"
1565 ))));
1566 };
1567
1568 let Segment::Line(new_line) = new_line_segment else {
1569 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1570 "Segment is not a line: {new_line_segment:?}"
1571 ))));
1572 };
1573
1574 let new_line_start_point_id = new_line.start;
1575
1576 let coincident = Coincident {
1578 segments: vec![previous_segment_end_point_id.into(), new_line_start_point_id.into()],
1579 };
1580
1581 let (final_src_delta, final_scene_delta) = self
1582 .add_constraint(ctx, version, sketch, Constraint::Coincident(coincident))
1583 .await?;
1584
1585 let mut combined_new_objects = first_scene_delta.new_objects.clone();
1588 combined_new_objects.extend(final_scene_delta.new_objects);
1589
1590 let scene_graph_delta = SceneGraphDelta {
1591 new_graph: self.scene_graph_for_ui(),
1592 invalidates_ids: false,
1593 new_objects: combined_new_objects,
1594 exec_outcome: final_scene_delta.exec_outcome,
1595 };
1596
1597 Ok((final_src_delta, scene_graph_delta))
1598 }
1599
1600 async fn edit_constraint_value(
1602 &mut self,
1603 ctx: &ExecutorContext,
1604 _version: Version,
1605 sketch: ObjectId,
1606 constraint_id: ObjectId,
1607 value_expression: String,
1608 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1609 let sketch_block_ref =
1611 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1612
1613 let object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
1614 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Object not found: {constraint_id:?}")))
1615 })?;
1616 if !matches!(&object.kind, ObjectKind::Constraint { .. }) {
1617 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1618 "Object is not a constraint: {constraint_id:?}"
1619 ))));
1620 }
1621
1622 let mut new_ast = self.program.ast.clone();
1623
1624 let (parsed, errors) = Program::parse(&value_expression).map_err(|e| {
1626 KclErrorWithOutputs::no_outputs(KclError::refactor(format_kcl_error_message(
1627 "Invalid constraint value",
1628 &e,
1629 )))
1630 })?;
1631 if !errors.is_empty() {
1632 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1633 format_compilation_issues("Invalid constraint value", &errors),
1634 )));
1635 }
1636 let mut parsed = parsed.ok_or_else(|| {
1637 KclErrorWithOutputs::no_outputs(KclError::refactor("No AST produced from value expression".to_string()))
1638 })?;
1639 if parsed.ast.body.is_empty() {
1640 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1641 "Empty value expression".to_string(),
1642 )));
1643 }
1644 let first = parsed.ast.body.remove(0);
1645 let ast::BodyItem::ExpressionStatement(expr_stmt) = first else {
1646 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1647 "Value expression must be a simple expression".to_string(),
1648 )));
1649 };
1650
1651 let new_value: ast::BinaryPart = expr_stmt
1652 .inner
1653 .expression
1654 .try_into()
1655 .map_err(|e: String| KclErrorWithOutputs::no_outputs(KclError::refactor(e)))?;
1656
1657 self.mutate_ast(
1658 &mut new_ast,
1659 constraint_id,
1660 AstMutateCommand::EditConstraintValue { value: new_value },
1661 )
1662 .map_err(KclErrorWithOutputs::no_outputs)?;
1663
1664 self.execute_after_edit(
1665 ctx,
1666 sketch,
1667 sketch_block_ref,
1668 &mut new_ast,
1669 ExecuteAfterEditOptions {
1670 segment_ids_edited: Default::default(),
1671 edit_kind: EditDeleteKind::Edit,
1672 commit_solved_initial_guesses: true,
1673 },
1674 )
1675 .await
1676 }
1677
1678 async fn edit_distance_constraint_label_position(
1679 &mut self,
1680 ctx: &ExecutorContext,
1681 _version: Version,
1682 sketch: ObjectId,
1683 constraint_id: ObjectId,
1684 label_position: Point2d<Number>,
1685 anchor_segment_ids: Vec<ObjectId>,
1686 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1687 let sketch_block_ref =
1689 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1690
1691 let mut new_ast = self.program.ast.clone();
1692 self.mutate_constraint_label_position(&mut new_ast, constraint_id, label_position)
1693 .map_err(KclErrorWithOutputs::no_outputs)?;
1694 let commit_solved_initial_guesses = self.next_edit_commits_solver_solutions.take().unwrap_or(true);
1695
1696 self.execute_after_edit(
1697 ctx,
1698 sketch,
1699 sketch_block_ref,
1700 &mut new_ast,
1701 ExecuteAfterEditOptions {
1702 segment_ids_edited: anchor_segment_ids.into_iter().collect(),
1703 edit_kind: EditDeleteKind::Edit,
1704 commit_solved_initial_guesses,
1705 },
1706 )
1707 .await
1708 }
1709
1710 async fn batch_split_segment_operations(
1718 &mut self,
1719 ctx: &ExecutorContext,
1720 _version: Version,
1721 sketch: ObjectId,
1722 edit_segments: Vec<ExistingSegmentCtor>,
1723 add_constraints: Vec<Constraint>,
1724 delete_constraint_ids: Vec<ObjectId>,
1725 _new_segment_info: sketch::NewSegmentInfo,
1726 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1727 let sketch_block_ref =
1729 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1730
1731 let mut new_ast = self.program.ast.clone();
1732 let mut segment_ids_edited = AhashIndexSet::with_capacity_and_hasher(edit_segments.len(), Default::default());
1733
1734 for segment in edit_segments {
1736 segment_ids_edited.insert(segment.id);
1737 match segment.ctor {
1738 SegmentCtor::Point(ctor) => self
1739 .edit_point(&mut new_ast, sketch, segment.id, ctor)
1740 .map_err(KclErrorWithOutputs::no_outputs)?,
1741 SegmentCtor::Line(ctor) => self
1742 .edit_line(&mut new_ast, sketch, segment.id, ctor)
1743 .map_err(KclErrorWithOutputs::no_outputs)?,
1744 SegmentCtor::Arc(ctor) => self
1745 .edit_arc(&mut new_ast, sketch, segment.id, ctor)
1746 .map_err(KclErrorWithOutputs::no_outputs)?,
1747 SegmentCtor::Circle(ctor) => self
1748 .edit_circle(&mut new_ast, sketch, segment.id, ctor)
1749 .map_err(KclErrorWithOutputs::no_outputs)?,
1750 SegmentCtor::ControlPointSpline(ctor) => self
1751 .edit_control_point_spline(&mut new_ast, sketch, segment.id, ctor)
1752 .map_err(KclErrorWithOutputs::no_outputs)?,
1753 }
1754 }
1755
1756 for constraint in add_constraints {
1758 match constraint {
1759 Constraint::Coincident(coincident) => {
1760 self.add_coincident(sketch, coincident, &mut new_ast)
1761 .await
1762 .map_err(KclErrorWithOutputs::no_outputs)?;
1763 }
1764 Constraint::Distance(distance) => {
1765 self.add_distance(sketch, distance, &mut new_ast)
1766 .await
1767 .map_err(KclErrorWithOutputs::no_outputs)?;
1768 }
1769 Constraint::EqualRadius(equal_radius) => {
1770 self.add_equal_radius(sketch, equal_radius, &mut new_ast)
1771 .await
1772 .map_err(KclErrorWithOutputs::no_outputs)?;
1773 }
1774 Constraint::Fixed(fixed) => {
1775 self.add_fixed_constraints(sketch, fixed.points, &mut new_ast)
1776 .await
1777 .map_err(KclErrorWithOutputs::no_outputs)?;
1778 }
1779 Constraint::HorizontalDistance(distance) => {
1780 self.add_horizontal_distance(sketch, distance, &mut new_ast)
1781 .await
1782 .map_err(KclErrorWithOutputs::no_outputs)?;
1783 }
1784 Constraint::VerticalDistance(distance) => {
1785 self.add_vertical_distance(sketch, distance, &mut new_ast)
1786 .await
1787 .map_err(KclErrorWithOutputs::no_outputs)?;
1788 }
1789 Constraint::Horizontal(horizontal) => {
1790 self.add_horizontal(sketch, horizontal, &mut new_ast)
1791 .await
1792 .map_err(KclErrorWithOutputs::no_outputs)?;
1793 }
1794 Constraint::LinesEqualLength(lines_equal_length) => {
1795 self.add_lines_equal_length(sketch, lines_equal_length, &mut new_ast)
1796 .await
1797 .map_err(KclErrorWithOutputs::no_outputs)?;
1798 }
1799 Constraint::Midpoint(midpoint) => {
1800 self.add_midpoint(sketch, midpoint, &mut new_ast)
1801 .await
1802 .map_err(KclErrorWithOutputs::no_outputs)?;
1803 }
1804 Constraint::Parallel(parallel) => {
1805 self.add_parallel(sketch, parallel, &mut new_ast)
1806 .await
1807 .map_err(KclErrorWithOutputs::no_outputs)?;
1808 }
1809 Constraint::Perpendicular(perpendicular) => {
1810 self.add_perpendicular(sketch, perpendicular, &mut new_ast)
1811 .await
1812 .map_err(KclErrorWithOutputs::no_outputs)?;
1813 }
1814 Constraint::Vertical(vertical) => {
1815 self.add_vertical(sketch, vertical, &mut new_ast)
1816 .await
1817 .map_err(KclErrorWithOutputs::no_outputs)?;
1818 }
1819 Constraint::Diameter(diameter) => {
1820 self.add_diameter(sketch, diameter, &mut new_ast)
1821 .await
1822 .map_err(KclErrorWithOutputs::no_outputs)?;
1823 }
1824 Constraint::Radius(radius) => {
1825 self.add_radius(sketch, radius, &mut new_ast)
1826 .await
1827 .map_err(KclErrorWithOutputs::no_outputs)?;
1828 }
1829 Constraint::Symmetric(symmetric) => {
1830 self.add_symmetric(sketch, symmetric, &mut new_ast)
1831 .await
1832 .map_err(KclErrorWithOutputs::no_outputs)?;
1833 }
1834 Constraint::Angle(angle) => {
1835 self.add_angle(sketch, angle, &mut new_ast)
1836 .await
1837 .map_err(KclErrorWithOutputs::no_outputs)?;
1838 }
1839 Constraint::Tangent(tangent) => {
1840 self.add_tangent(sketch, tangent, &mut new_ast)
1841 .await
1842 .map_err(KclErrorWithOutputs::no_outputs)?;
1843 }
1844 }
1845 }
1846
1847 let constraint_ids_set = delete_constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
1849
1850 let has_constraint_deletions = !constraint_ids_set.is_empty();
1851 for constraint_id in constraint_ids_set {
1852 self.delete_constraint(&mut new_ast, sketch, constraint_id)
1853 .map_err(KclErrorWithOutputs::no_outputs)?;
1854 }
1855
1856 let (source_delta, mut scene_graph_delta) = self
1860 .execute_after_edit(
1861 ctx,
1862 sketch,
1863 sketch_block_ref,
1864 &mut new_ast,
1865 ExecuteAfterEditOptions {
1866 segment_ids_edited,
1867 edit_kind: EditDeleteKind::Edit,
1868 commit_solved_initial_guesses: true,
1869 },
1870 )
1871 .await?;
1872
1873 if has_constraint_deletions {
1876 scene_graph_delta.invalidates_ids = true;
1877 }
1878
1879 Ok((source_delta, scene_graph_delta))
1880 }
1881
1882 async fn batch_tail_cut_operations(
1883 &mut self,
1884 ctx: &ExecutorContext,
1885 _version: Version,
1886 sketch: ObjectId,
1887 edit_segments: Vec<ExistingSegmentCtor>,
1888 add_constraints: Vec<Constraint>,
1889 delete_constraint_ids: Vec<ObjectId>,
1890 additional_edited_segment_ids: Vec<ObjectId>,
1891 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1892 let sketch_block_ref =
1893 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1894
1895 let mut new_ast = self.program.ast.clone();
1896 let mut segment_ids_edited = AhashIndexSet::with_capacity_and_hasher(edit_segments.len(), Default::default());
1897
1898 for segment in edit_segments {
1900 segment_ids_edited.insert(segment.id);
1901 match segment.ctor {
1902 SegmentCtor::Point(ctor) => self
1903 .edit_point(&mut new_ast, sketch, segment.id, ctor)
1904 .map_err(KclErrorWithOutputs::no_outputs)?,
1905 SegmentCtor::Line(ctor) => self
1906 .edit_line(&mut new_ast, sketch, segment.id, ctor)
1907 .map_err(KclErrorWithOutputs::no_outputs)?,
1908 SegmentCtor::Arc(ctor) => self
1909 .edit_arc(&mut new_ast, sketch, segment.id, ctor)
1910 .map_err(KclErrorWithOutputs::no_outputs)?,
1911 SegmentCtor::Circle(ctor) => self
1912 .edit_circle(&mut new_ast, sketch, segment.id, ctor)
1913 .map_err(KclErrorWithOutputs::no_outputs)?,
1914 SegmentCtor::ControlPointSpline(ctor) => self
1915 .edit_control_point_spline(&mut new_ast, sketch, segment.id, ctor)
1916 .map_err(KclErrorWithOutputs::no_outputs)?,
1917 }
1918 }
1919
1920 segment_ids_edited.extend(additional_edited_segment_ids);
1921
1922 for constraint in add_constraints {
1924 match constraint {
1925 Constraint::Coincident(coincident) => {
1926 self.add_coincident(sketch, coincident, &mut new_ast)
1927 .await
1928 .map_err(KclErrorWithOutputs::no_outputs)?;
1929 }
1930 other => {
1931 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1932 "unsupported constraint in tail cut batch: {other:?}"
1933 ))));
1934 }
1935 }
1936 }
1937
1938 let constraint_ids_set = delete_constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
1940
1941 let has_constraint_deletions = !constraint_ids_set.is_empty();
1942 for constraint_id in constraint_ids_set {
1943 self.delete_constraint(&mut new_ast, sketch, constraint_id)
1944 .map_err(KclErrorWithOutputs::no_outputs)?;
1945 }
1946
1947 let (source_delta, mut scene_graph_delta) = self
1951 .execute_after_edit(
1952 ctx,
1953 sketch,
1954 sketch_block_ref,
1955 &mut new_ast,
1956 ExecuteAfterEditOptions {
1957 segment_ids_edited,
1958 edit_kind: EditDeleteKind::Edit,
1959 commit_solved_initial_guesses: true,
1960 },
1961 )
1962 .await?;
1963
1964 if has_constraint_deletions {
1967 scene_graph_delta.invalidates_ids = true;
1968 }
1969
1970 Ok((source_delta, scene_graph_delta))
1971 }
1972}
1973
1974impl FrontendState {
1975 pub async fn evaluate_expression(&self, ctx: &ExecutorContext, mut program: Program) -> ExecResult<ExecOutcome> {
1977 program.ast.inner_attrs.clone_from(&self.program.ast.inner_attrs);
1978 ctx.run_mock(&program, &MockConfig::default()).await
1979 }
1980
1981 pub async fn hack_set_program(&mut self, ctx: &ExecutorContext, program: Program) -> ExecResult<SetProgramOutcome> {
1982 self.program = program.clone();
1983
1984 self.point_freedom_cache.clear();
1995 match ctx.run_with_caching(program).await {
1996 Ok(outcome) => {
1997 let outcome = self.update_state_after_exec(outcome, true);
1998 let checkpoint_id = self
1999 .create_sketch_checkpoint(&ctx.cache, outcome.clone())
2000 .await
2001 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
2002 Ok(SetProgramOutcome::Success {
2003 scene_graph: Box::new(self.scene_graph_for_ui()),
2004 exec_outcome: Box::new(outcome),
2005 checkpoint_id: Some(checkpoint_id),
2006 })
2007 }
2008 Err(mut err) => {
2009 let outcome = self.exec_outcome_from_exec_error(err.clone())?;
2012 self.update_state_after_exec(outcome, true);
2013 err.scene_graph = Some(self.scene_graph_for_ui());
2014 Ok(SetProgramOutcome::ExecFailure { error: Box::new(err) })
2015 }
2016 }
2017 }
2018
2019 pub async fn engine_execute(
2022 &mut self,
2023 ctx: &ExecutorContext,
2024 program: Program,
2025 ) -> Result<SceneGraphDelta, KclErrorWithOutputs> {
2026 self.program = program.clone();
2027
2028 self.point_freedom_cache.clear();
2032 match ctx.run_with_caching(program).await {
2033 Ok(outcome) => {
2034 let outcome = self.update_state_after_exec(outcome, true);
2035 Ok(SceneGraphDelta {
2036 new_graph: self.scene_graph_for_ui(),
2037 exec_outcome: outcome,
2038 new_objects: Default::default(),
2040 invalidates_ids: Default::default(),
2042 })
2043 }
2044 Err(mut err) => {
2045 let outcome = self.exec_outcome_from_exec_error(err.clone())?;
2047 self.update_state_after_exec(outcome, true);
2048 err.scene_graph = Some(self.scene_graph_for_ui());
2049 Err(err)
2050 }
2051 }
2052 }
2053
2054 fn exec_outcome_from_exec_error(&self, err: KclErrorWithOutputs) -> Result<ExecOutcome, KclErrorWithOutputs> {
2055 if matches!(err.error, KclError::EngineHangup { .. }) {
2056 return Err(err);
2060 }
2061
2062 let KclErrorWithOutputs {
2063 error,
2064 mut non_fatal,
2065 variables,
2066 operations,
2067 artifact_graph,
2068 scene_objects,
2069 source_range_to_object,
2070 var_solutions,
2071 refactor_metadata,
2072 filenames,
2073 source_files,
2074 default_planes,
2075 engine_scene_generation,
2076 ..
2077 } = err;
2078
2079 non_fatal.push(CompilationIssue::fatal(issue_source_range(&error), error.get_message()));
2080
2081 Ok(ExecOutcome {
2082 variables,
2083 #[cfg(test)]
2084 test_program_memory: Default::default(),
2085 filenames,
2086 engine_scene_generation: engine_scene_generation.unwrap_or_default(),
2090 operations,
2091 artifact_graph,
2092 scene_objects,
2093 source_range_to_object,
2094 var_solutions,
2095 refactor_metadata,
2096 issues: non_fatal,
2097 source_files,
2098 default_planes,
2099 })
2100 }
2101
2102 async fn add_point(
2103 &mut self,
2104 ctx: &ExecutorContext,
2105 sketch: ObjectId,
2106 ctor: PointCtor,
2107 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2108 let at_ast = to_ast_point2d(&ctor.position)
2110 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2111 let point_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
2112 callee: ast::Node::no_src(ast_sketch2_name(POINT_FN)),
2113 unlabeled: None,
2114 arguments: vec![ast::LabeledArg {
2115 label: Some(ast::Identifier::new(POINT_AT_PARAM)),
2116 arg: at_ast,
2117 }],
2118 digest: None,
2119 non_code_meta: Default::default(),
2120 })));
2121
2122 let sketch_id = sketch;
2124 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2125 #[cfg(target_arch = "wasm32")]
2126 web_sys::console::error_1(
2127 &format!(
2128 "Sketch not found; sketch_id={sketch_id:?}, self.scene_graph.objects={:#?}",
2129 self.scene_graph.objects
2130 )
2131 .into(),
2132 );
2133 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2134 })?;
2135 let ObjectKind::Sketch(_) = &sketch_object.kind else {
2136 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2137 "Object is not a sketch, it is {}",
2138 sketch_object.kind.human_friendly_kind_with_article(),
2139 ))));
2140 };
2141 let mut new_ast = self.program.ast.clone();
2143 let (sketch_block_ref, _) = self
2144 .mutate_ast(
2145 &mut new_ast,
2146 sketch_id,
2147 AstMutateCommand::AddSketchBlockExprStmt { expr: point_ast },
2148 )
2149 .map_err(KclErrorWithOutputs::no_outputs)?;
2150 let new_source = source_from_ast(&new_ast);
2152 let new_program = parse_frontend_mutation_source(
2154 &new_source,
2155 "Error parsing KCL source after adding point",
2156 "No AST produced after adding point",
2157 )?;
2158
2159 let point_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2160 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2161 "Source range of point not found in sketch block: {sketch_block_ref:?}; {err:?}"
2162 )))
2163 })?;
2164
2165 self.program = new_program.clone();
2167
2168 let mut truncated_program = new_program;
2170 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2171 .map_err(KclErrorWithOutputs::no_outputs)?;
2172
2173 let outcome = ctx
2175 .run_mock(
2176 &truncated_program,
2177 &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2178 )
2179 .await?;
2180
2181 let new_object_ids = {
2182 let make_err =
2183 |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2184 let segment_id = outcome
2185 .source_range_to_object
2186 .get(&point_node_ref.range)
2187 .copied()
2188 .ok_or_else(|| make_err(format!("Source range of point not found: {point_node_ref:?}")))?;
2189 let segment_object = outcome
2190 .scene_objects
2191 .get(segment_id.0)
2192 .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2193 let ObjectKind::Segment { segment } = &segment_object.kind else {
2194 return Err(make_err(format!(
2195 "Object is not a segment, it is {}",
2196 segment_object.kind.human_friendly_kind_with_article()
2197 )));
2198 };
2199 let Segment::Point(_) = segment else {
2200 return Err(make_err(format!(
2201 "Segment is not a point, it is {}",
2202 segment.human_friendly_kind_with_article()
2203 )));
2204 };
2205 vec![segment_id]
2206 };
2207 let src_delta = SourceDelta { text: new_source };
2208 let outcome = self.update_state_after_exec(outcome, false);
2210 let scene_graph_delta = SceneGraphDelta {
2211 new_graph: self.scene_graph_for_ui(),
2212 invalidates_ids: false,
2213 new_objects: new_object_ids,
2214 exec_outcome: outcome,
2215 };
2216 Ok((src_delta, scene_graph_delta))
2217 }
2218
2219 async fn add_line(
2220 &mut self,
2221 ctx: &ExecutorContext,
2222 sketch: ObjectId,
2223 ctor: LineCtor,
2224 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2225 let start_ast = to_ast_point2d(&ctor.start)
2227 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2228 let end_ast = to_ast_point2d(&ctor.end)
2229 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2230 let mut arguments = vec![
2231 ast::LabeledArg {
2232 label: Some(ast::Identifier::new(LINE_START_PARAM)),
2233 arg: start_ast,
2234 },
2235 ast::LabeledArg {
2236 label: Some(ast::Identifier::new(LINE_END_PARAM)),
2237 arg: end_ast,
2238 },
2239 ];
2240 if ctor.construction == Some(true) {
2242 arguments.push(ast::LabeledArg {
2243 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2244 arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
2245 value: ast::LiteralValue::Bool(true),
2246 raw: "true".to_string(),
2247 digest: None,
2248 }))),
2249 });
2250 }
2251 let line_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
2252 callee: ast::Node::no_src(ast_sketch2_name(LINE_FN)),
2253 unlabeled: None,
2254 arguments,
2255 digest: None,
2256 non_code_meta: Default::default(),
2257 })));
2258
2259 let sketch_id = sketch;
2261 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2262 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2263 })?;
2264 let ObjectKind::Sketch(_) = &sketch_object.kind else {
2265 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2266 "Object is not a sketch, it is {}",
2267 sketch_object.kind.human_friendly_kind_with_article(),
2268 ))));
2269 };
2270 let mut new_ast = self.program.ast.clone();
2272 let (sketch_block_ref, _) = self
2273 .mutate_ast(
2274 &mut new_ast,
2275 sketch_id,
2276 AstMutateCommand::AddSketchBlockExprStmt { expr: line_ast },
2277 )
2278 .map_err(KclErrorWithOutputs::no_outputs)?;
2279 let new_source = source_from_ast(&new_ast);
2281 let new_program = parse_frontend_mutation_source(
2283 &new_source,
2284 "Error parsing KCL source after adding line",
2285 "No AST produced after adding line",
2286 )?;
2287
2288 let line_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2289 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2290 "Source range of line not found in sketch block: {sketch_block_ref:?}; {err:?}"
2291 )))
2292 })?;
2293
2294 self.program = new_program.clone();
2296
2297 let mut truncated_program = new_program;
2299 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2300 .map_err(KclErrorWithOutputs::no_outputs)?;
2301
2302 let outcome = ctx
2304 .run_mock(
2305 &truncated_program,
2306 &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2307 )
2308 .await?;
2309
2310 let new_object_ids = {
2311 let make_err =
2312 |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2313 let segment_id = outcome
2314 .source_range_to_object
2315 .get(&line_node_ref.range)
2316 .copied()
2317 .ok_or_else(|| make_err(format!("Source range of line not found: {line_node_ref:?}")))?;
2318 let segment_object = outcome
2319 .scene_object_by_id(segment_id)
2320 .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2321 let ObjectKind::Segment { segment } = &segment_object.kind else {
2322 return Err(make_err(format!(
2323 "Object is not a segment, it is {}",
2324 segment_object.kind.human_friendly_kind_with_article()
2325 )));
2326 };
2327 let Segment::Line(line) = segment else {
2328 return Err(make_err(format!(
2329 "Segment is not a line, it is {}",
2330 segment.human_friendly_kind_with_article()
2331 )));
2332 };
2333 vec![line.start, line.end, segment_id]
2334 };
2335 let src_delta = SourceDelta { text: new_source };
2336 let outcome = self.update_state_after_exec(outcome, false);
2338 let scene_graph_delta = SceneGraphDelta {
2339 new_graph: self.scene_graph_for_ui(),
2340 invalidates_ids: false,
2341 new_objects: new_object_ids,
2342 exec_outcome: outcome,
2343 };
2344 Ok((src_delta, scene_graph_delta))
2345 }
2346
2347 async fn add_arc(
2348 &mut self,
2349 ctx: &ExecutorContext,
2350 sketch: ObjectId,
2351 ctor: ArcCtor,
2352 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2353 let start_ast = to_ast_point2d(&ctor.start)
2355 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2356 let end_ast = to_ast_point2d(&ctor.end)
2357 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2358 let center_ast = to_ast_point2d(&ctor.center)
2359 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2360 let mut arguments = vec![
2361 ast::LabeledArg {
2362 label: Some(ast::Identifier::new(ARC_START_PARAM)),
2363 arg: start_ast,
2364 },
2365 ast::LabeledArg {
2366 label: Some(ast::Identifier::new(ARC_END_PARAM)),
2367 arg: end_ast,
2368 },
2369 ast::LabeledArg {
2370 label: Some(ast::Identifier::new(ARC_CENTER_PARAM)),
2371 arg: center_ast,
2372 },
2373 ];
2374 if ctor.direction == Some(ArcDirection::Cw) {
2377 arguments.push(ast::LabeledArg {
2378 label: Some(ast::Identifier::new(ARC_DIRECTION_PARAM)),
2379 arg: ast::Expr::Name(BoxNode::new(ast::Name::new(ARC_DIRECTION_CW_NAME))),
2380 });
2381 }
2382 if ctor.construction == Some(true) {
2384 arguments.push(ast::LabeledArg {
2385 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2386 arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
2387 value: ast::LiteralValue::Bool(true),
2388 raw: "true".to_string(),
2389 digest: None,
2390 }))),
2391 });
2392 }
2393 let arc_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
2394 callee: ast::Node::no_src(ast_sketch2_name(ARC_FN)),
2395 unlabeled: None,
2396 arguments,
2397 digest: None,
2398 non_code_meta: Default::default(),
2399 })));
2400
2401 let sketch_id = sketch;
2403 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2404 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2405 })?;
2406 let ObjectKind::Sketch(_) = &sketch_object.kind else {
2407 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2408 "Object is not a sketch, it is {}",
2409 sketch_object.kind.human_friendly_kind_with_article(),
2410 ))));
2411 };
2412 let mut new_ast = self.program.ast.clone();
2414 let (sketch_block_ref, _) = self
2415 .mutate_ast(
2416 &mut new_ast,
2417 sketch_id,
2418 AstMutateCommand::AddSketchBlockExprStmt { expr: arc_ast },
2419 )
2420 .map_err(KclErrorWithOutputs::no_outputs)?;
2421 let new_source = source_from_ast(&new_ast);
2423 let new_program = parse_frontend_mutation_source(
2425 &new_source,
2426 "Error parsing KCL source after adding arc",
2427 "No AST produced after adding arc",
2428 )?;
2429
2430 let arc_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2431 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2432 "Source range of arc not found in sketch block: {sketch_block_ref:?}; {err:?}"
2433 )))
2434 })?;
2435
2436 self.program = new_program.clone();
2438
2439 let mut truncated_program = new_program;
2441 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2442 .map_err(KclErrorWithOutputs::no_outputs)?;
2443
2444 let outcome = ctx
2446 .run_mock(
2447 &truncated_program,
2448 &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2449 )
2450 .await?;
2451
2452 let new_object_ids = {
2453 let make_err =
2454 |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2455 let segment_id = outcome
2456 .source_range_to_object
2457 .get(&arc_node_ref.range)
2458 .copied()
2459 .ok_or_else(|| make_err(format!("Source range of arc not found: {arc_node_ref:?}")))?;
2460 let segment_object = outcome
2461 .scene_objects
2462 .get(segment_id.0)
2463 .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2464 let ObjectKind::Segment { segment } = &segment_object.kind else {
2465 return Err(make_err(format!(
2466 "Object is not a segment, it is {}",
2467 segment_object.kind.human_friendly_kind_with_article()
2468 )));
2469 };
2470 let Segment::Arc(arc) = segment else {
2471 return Err(make_err(format!(
2472 "Segment is not an arc, it is {}",
2473 segment.human_friendly_kind_with_article()
2474 )));
2475 };
2476 vec![arc.start, arc.end, arc.center, segment_id]
2477 };
2478 let src_delta = SourceDelta { text: new_source };
2479 let outcome = self.update_state_after_exec(outcome, false);
2481 let scene_graph_delta = SceneGraphDelta {
2482 new_graph: self.scene_graph_for_ui(),
2483 invalidates_ids: false,
2484 new_objects: new_object_ids,
2485 exec_outcome: outcome,
2486 };
2487 Ok((src_delta, scene_graph_delta))
2488 }
2489
2490 async fn add_circle(
2491 &mut self,
2492 ctx: &ExecutorContext,
2493 sketch: ObjectId,
2494 ctor: CircleCtor,
2495 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2496 let start_ast = to_ast_point2d(&ctor.start)
2498 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2499 let center_ast = to_ast_point2d(&ctor.center)
2500 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2501 let mut arguments = vec![
2502 ast::LabeledArg {
2503 label: Some(ast::Identifier::new(CIRCLE_START_PARAM)),
2504 arg: start_ast,
2505 },
2506 ast::LabeledArg {
2507 label: Some(ast::Identifier::new(CIRCLE_CENTER_PARAM)),
2508 arg: center_ast,
2509 },
2510 ];
2511 if ctor.construction == Some(true) {
2513 arguments.push(ast::LabeledArg {
2514 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2515 arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
2516 value: ast::LiteralValue::Bool(true),
2517 raw: "true".to_string(),
2518 digest: None,
2519 }))),
2520 });
2521 }
2522 let circle_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
2523 callee: ast::Node::no_src(ast_sketch2_name(CIRCLE_FN)),
2524 unlabeled: None,
2525 arguments,
2526 digest: None,
2527 non_code_meta: Default::default(),
2528 })));
2529
2530 let sketch_id = sketch;
2532 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2533 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2534 })?;
2535 let ObjectKind::Sketch(_) = &sketch_object.kind else {
2536 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2537 "Object is not a sketch, it is {}",
2538 sketch_object.kind.human_friendly_kind_with_article(),
2539 ))));
2540 };
2541 let mut new_ast = self.program.ast.clone();
2543 let (sketch_block_ref, _) = self
2544 .mutate_ast(
2545 &mut new_ast,
2546 sketch_id,
2547 AstMutateCommand::AddSketchBlockVarDecl {
2548 prefix: CIRCLE_VARIABLE.to_owned(),
2549 expr: circle_ast,
2550 },
2551 )
2552 .map_err(KclErrorWithOutputs::no_outputs)?;
2553 let new_source = source_from_ast(&new_ast);
2555 let new_program = parse_frontend_mutation_source(
2557 &new_source,
2558 "Error parsing KCL source after adding circle",
2559 "No AST produced after adding circle",
2560 )?;
2561
2562 let circle_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2563 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2564 "Source range of circle not found in sketch block: {sketch_block_ref:?}; {err:?}"
2565 )))
2566 })?;
2567
2568 self.program = new_program.clone();
2570
2571 let mut truncated_program = new_program;
2573 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2574 .map_err(KclErrorWithOutputs::no_outputs)?;
2575
2576 let outcome = ctx
2578 .run_mock(
2579 &truncated_program,
2580 &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2581 )
2582 .await?;
2583
2584 let new_object_ids = {
2585 let make_err =
2586 |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2587 let segment_id = outcome
2588 .source_range_to_object
2589 .get(&circle_node_ref.range)
2590 .copied()
2591 .ok_or_else(|| make_err(format!("Source range of circle not found: {circle_node_ref:?}")))?;
2592 let segment_object = outcome
2593 .scene_objects
2594 .get(segment_id.0)
2595 .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2596 let ObjectKind::Segment { segment } = &segment_object.kind else {
2597 return Err(make_err(format!(
2598 "Object is not a segment, it is {}",
2599 segment_object.kind.human_friendly_kind_with_article()
2600 )));
2601 };
2602 let Segment::Circle(circle) = segment else {
2603 return Err(make_err(format!(
2604 "Segment is not a circle, it is {}",
2605 segment.human_friendly_kind_with_article()
2606 )));
2607 };
2608 vec![circle.start, circle.center, segment_id]
2609 };
2610 let src_delta = SourceDelta { text: new_source };
2611 let outcome = self.update_state_after_exec(outcome, false);
2613 let scene_graph_delta = SceneGraphDelta {
2614 new_graph: self.scene_graph_for_ui(),
2615 invalidates_ids: false,
2616 new_objects: new_object_ids,
2617 exec_outcome: outcome,
2618 };
2619 Ok((src_delta, scene_graph_delta))
2620 }
2621
2622 async fn add_control_point_spline(
2623 &mut self,
2624 ctx: &ExecutorContext,
2625 sketch: ObjectId,
2626 ctor: ControlPointSplineCtor,
2627 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2628 let new_program = ensure_control_point_spline_experimental_features(&self.program)
2629 .map_err(KclErrorWithOutputs::no_outputs)?;
2630
2631 let points_ast = to_ast_point2d_array(&ctor.points)
2632 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2633 let mut arguments = vec![ast::LabeledArg {
2634 label: Some(ast::Identifier::new(CONTROL_POINT_SPLINE_POINTS_PARAM)),
2635 arg: points_ast,
2636 }];
2637 if ctor.construction == Some(true) {
2638 arguments.push(ast::LabeledArg {
2639 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2640 arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
2641 value: ast::LiteralValue::Bool(true),
2642 raw: "true".to_string(),
2643 digest: None,
2644 }))),
2645 });
2646 }
2647 let spline_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
2648 callee: ast::Node::no_src(ast_sketch2_name(CONTROL_POINT_SPLINE_FN)),
2649 unlabeled: None,
2650 arguments,
2651 digest: None,
2652 non_code_meta: Default::default(),
2653 })));
2654
2655 let sketch_object = self.scene_graph.objects.get(sketch.0).ok_or_else(|| {
2656 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2657 })?;
2658 let ObjectKind::Sketch(_) = &sketch_object.kind else {
2659 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2660 "Object is not a sketch, it is {}",
2661 sketch_object.kind.human_friendly_kind_with_article(),
2662 ))));
2663 };
2664
2665 let mut new_ast = new_program.ast.clone();
2666 let (sketch_block_ref, _) = self
2667 .mutate_ast(
2668 &mut new_ast,
2669 sketch,
2670 AstMutateCommand::AddSketchBlockExprStmt { expr: spline_ast },
2671 )
2672 .map_err(KclErrorWithOutputs::no_outputs)?;
2673 let new_source = source_from_ast(&new_ast);
2674 let new_program = parse_frontend_mutation_source(
2675 &new_source,
2676 "Error parsing KCL source after adding controlPointSpline",
2677 "No AST produced after adding controlPointSpline",
2678 )?;
2679
2680 let spline_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2681 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2682 "Source range of controlPointSpline not found in sketch block: {sketch_block_ref:?}; {err:?}"
2683 )))
2684 })?;
2685
2686 self.program = new_program.clone();
2687
2688 let mut truncated_program = new_program;
2689 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2690 .map_err(KclErrorWithOutputs::no_outputs)?;
2691
2692 let outcome = ctx
2693 .run_mock(
2694 &truncated_program,
2695 &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2696 )
2697 .await?;
2698
2699 let new_object_ids = {
2700 let make_err =
2701 |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2702 let segment_id = outcome
2703 .source_range_to_object
2704 .get(&spline_node_ref.range)
2705 .copied()
2706 .ok_or_else(|| {
2707 make_err(format!(
2708 "Source range of controlPointSpline not found: {spline_node_ref:?}"
2709 ))
2710 })?;
2711 let segment_object = outcome
2712 .scene_objects
2713 .get(segment_id.0)
2714 .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2715 let ObjectKind::Segment { segment } = &segment_object.kind else {
2716 return Err(make_err(format!(
2717 "Object is not a segment, it is {}",
2718 segment_object.kind.human_friendly_kind_with_article()
2719 )));
2720 };
2721 let Segment::ControlPointSpline(spline) = segment else {
2722 return Err(make_err(format!(
2723 "Segment is not a control point spline, it is {}",
2724 segment.human_friendly_kind_with_article()
2725 )));
2726 };
2727
2728 let mut ids = outcome
2729 .scene_objects
2730 .iter()
2731 .filter_map(|obj| match &obj.kind {
2732 ObjectKind::Segment {
2733 segment: Segment::Line(line),
2734 } if line.owner == Some(segment_id) => Some(obj.id),
2735 _ => None,
2736 })
2737 .collect::<Vec<_>>();
2738 ids.extend(spline.controls.clone());
2739 ids.push(segment_id);
2740 ids
2741 };
2742 let src_delta = SourceDelta { text: new_source };
2743 let outcome = self.update_state_after_exec(outcome, false);
2744 let scene_graph_delta = SceneGraphDelta {
2745 new_graph: self.scene_graph_for_ui(),
2746 invalidates_ids: false,
2747 new_objects: new_object_ids,
2748 exec_outcome: outcome,
2749 };
2750 Ok((src_delta, scene_graph_delta))
2751 }
2752
2753 fn edit_point(
2754 &mut self,
2755 new_ast: &mut ast::Node<ast::Program>,
2756 sketch: ObjectId,
2757 point: ObjectId,
2758 ctor: PointCtor,
2759 ) -> Result<(), KclError> {
2760 let new_at_ast = to_ast_point2d(&ctor.position).map_err(|err| KclError::refactor(err.to_string()))?;
2762
2763 let sketch_id = sketch;
2765 let sketch_object = self
2766 .scene_graph
2767 .objects
2768 .get(sketch_id.0)
2769 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2770 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2771 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2772 };
2773 sketch.segments.iter().find(|o| **o == point).ok_or_else(|| {
2774 KclError::refactor(format!("Point not found in sketch: point={point:?}, sketch={sketch:?}"))
2775 })?;
2776 let point_id = point;
2778 let point_object = self
2779 .scene_graph
2780 .objects
2781 .get(point_id.0)
2782 .ok_or_else(|| KclError::refactor(format!("Point not found in scene graph: point={point:?}")))?;
2783 let ObjectKind::Segment {
2784 segment: Segment::Point(point),
2785 } = &point_object.kind
2786 else {
2787 return Err(KclError::refactor(format!(
2788 "Object is not a point segment: {point_object:?}"
2789 )));
2790 };
2791
2792 if let Some(owner_id) = point.owner {
2794 let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
2795 KclError::refactor(format!(
2796 "Internal: Owner of point not found in scene graph: owner={owner_id:?}",
2797 ))
2798 })?;
2799 let ObjectKind::Segment { segment } = &owner_object.kind else {
2800 return Err(KclError::refactor(format!(
2801 "Internal: Owner of point is not a segment, but found {}",
2802 owner_object.kind.human_friendly_kind_with_article()
2803 )));
2804 };
2805
2806 if let Segment::Line(line) = segment {
2808 let SegmentCtor::Line(line_ctor) = &line.ctor else {
2809 return Err(KclError::refactor(format!(
2810 "Internal: Owner of point does not have line ctor, but found {}",
2811 line.ctor.human_friendly_kind_with_article()
2812 )));
2813 };
2814 let mut line_ctor = line_ctor.clone();
2815 if line.start == point_id {
2817 line_ctor.start = ctor.position;
2818 } else if line.end == point_id {
2819 line_ctor.end = ctor.position;
2820 } else {
2821 return Err(KclError::refactor(format!(
2822 "Internal: Point is not part of owner's line segment: point={point_id:?}, line={owner_id:?}"
2823 )));
2824 }
2825 return self.edit_line(new_ast, sketch_id, owner_id, line_ctor);
2826 }
2827
2828 if let Segment::Arc(arc) = segment {
2830 let SegmentCtor::Arc(arc_ctor) = &arc.ctor else {
2831 return Err(KclError::refactor(format!(
2832 "Internal: Owner of point does not have arc ctor, but found {}",
2833 arc.ctor.human_friendly_kind_with_article()
2834 )));
2835 };
2836 let mut arc_ctor = arc_ctor.clone();
2837 if arc.center == point_id {
2839 arc_ctor.center = ctor.position;
2840 } else if arc.start == point_id {
2841 arc_ctor.start = ctor.position;
2842 } else if arc.end == point_id {
2843 arc_ctor.end = ctor.position;
2844 } else {
2845 return Err(KclError::refactor(format!(
2846 "Internal: Point is not part of owner's arc segment: point={point_id:?}, arc={owner_id:?}"
2847 )));
2848 }
2849 return self.edit_arc(new_ast, sketch_id, owner_id, arc_ctor);
2850 }
2851
2852 if let Segment::Circle(circle) = segment {
2854 let SegmentCtor::Circle(circle_ctor) = &circle.ctor else {
2855 return Err(KclError::refactor(format!(
2856 "Internal: Owner of point does not have circle ctor, but found {}",
2857 circle.ctor.human_friendly_kind_with_article()
2858 )));
2859 };
2860 let mut circle_ctor = circle_ctor.clone();
2861 if circle.center == point_id {
2862 circle_ctor.center = ctor.position;
2863 } else if circle.start == point_id {
2864 circle_ctor.start = ctor.position;
2865 } else {
2866 return Err(KclError::refactor(format!(
2867 "Internal: Point is not part of owner's circle segment: point={point_id:?}, circle={owner_id:?}"
2868 )));
2869 }
2870 return self.edit_circle(new_ast, sketch_id, owner_id, circle_ctor);
2871 }
2872
2873 if let Segment::ControlPointSpline(spline) = segment {
2874 let SegmentCtor::ControlPointSpline(spline_ctor) = &spline.ctor else {
2875 return Err(KclError::refactor(format!(
2876 "Internal: Owner of point does not have controlPointSpline ctor, but found {}",
2877 spline.ctor.human_friendly_kind_with_article()
2878 )));
2879 };
2880 let mut spline_ctor = spline_ctor.clone();
2881 let Some(control_index) = spline.controls.iter().position(|id| *id == point_id) else {
2882 return Err(KclError::refactor(format!(
2883 "Internal: Point is not part of owner's controlPointSpline segment: point={point_id:?}, spline={owner_id:?}"
2884 )));
2885 };
2886 spline_ctor.points[control_index] = ctor.position;
2887 return self.edit_control_point_spline(new_ast, sketch_id, owner_id, spline_ctor);
2888 }
2889
2890 }
2893
2894 self.mutate_ast(new_ast, point_id, AstMutateCommand::EditPoint { at: new_at_ast })?;
2896 Ok(())
2897 }
2898
2899 fn edit_line(
2900 &mut self,
2901 new_ast: &mut ast::Node<ast::Program>,
2902 sketch: ObjectId,
2903 line: ObjectId,
2904 ctor: LineCtor,
2905 ) -> Result<(), KclError> {
2906 let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2908 let new_end_ast = to_ast_point2d(&ctor.end).map_err(|err| KclError::refactor(err.to_string()))?;
2909
2910 let sketch_id = sketch;
2912 let sketch_object = self
2913 .scene_graph
2914 .objects
2915 .get(sketch_id.0)
2916 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2917 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2918 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2919 };
2920 sketch
2921 .segments
2922 .iter()
2923 .find(|o| **o == line)
2924 .ok_or_else(|| KclError::refactor(format!("Line not found in sketch: line={line:?}, sketch={sketch:?}")))?;
2925 let line_id = line;
2927 let line_object = self
2928 .scene_graph
2929 .objects
2930 .get(line_id.0)
2931 .ok_or_else(|| KclError::refactor(format!("Line not found in scene graph: line={line:?}")))?;
2932 let ObjectKind::Segment { .. } = &line_object.kind else {
2933 let kind = line_object.kind.human_friendly_kind_with_article();
2934 return Err(KclError::refactor(format!(
2935 "This constraint only works on Segments, but you selected {kind}"
2936 )));
2937 };
2938
2939 self.mutate_ast(
2941 new_ast,
2942 line_id,
2943 AstMutateCommand::EditLine {
2944 start: new_start_ast,
2945 end: new_end_ast,
2946 construction: ctor.construction,
2947 },
2948 )?;
2949 Ok(())
2950 }
2951
2952 fn edit_arc(
2953 &mut self,
2954 new_ast: &mut ast::Node<ast::Program>,
2955 sketch: ObjectId,
2956 arc: ObjectId,
2957 ctor: ArcCtor,
2958 ) -> Result<(), KclError> {
2959 let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2961 let new_end_ast = to_ast_point2d(&ctor.end).map_err(|err| KclError::refactor(err.to_string()))?;
2962 let new_center_ast = to_ast_point2d(&ctor.center).map_err(|err| KclError::refactor(err.to_string()))?;
2963
2964 let sketch_id = sketch;
2966 let sketch_object = self
2967 .scene_graph
2968 .objects
2969 .get(sketch_id.0)
2970 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2971 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2972 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2973 };
2974 sketch
2975 .segments
2976 .iter()
2977 .find(|o| **o == arc)
2978 .ok_or_else(|| KclError::refactor(format!("Arc not found in sketch: arc={arc:?}, sketch={sketch:?}")))?;
2979 let arc_id = arc;
2981 let arc_object = self
2982 .scene_graph
2983 .objects
2984 .get(arc_id.0)
2985 .ok_or_else(|| KclError::refactor(format!("Arc not found in scene graph: arc={arc:?}")))?;
2986 let ObjectKind::Segment { .. } = &arc_object.kind else {
2987 return Err(KclError::refactor(format!("Object is not a segment: {arc_object:?}")));
2988 };
2989
2990 self.mutate_ast(
2992 new_ast,
2993 arc_id,
2994 AstMutateCommand::EditArc {
2995 start: new_start_ast,
2996 end: new_end_ast,
2997 center: new_center_ast,
2998 direction: ctor.direction,
2999 construction: ctor.construction,
3000 },
3001 )?;
3002 Ok(())
3003 }
3004
3005 fn edit_circle(
3006 &mut self,
3007 new_ast: &mut ast::Node<ast::Program>,
3008 sketch: ObjectId,
3009 circle: ObjectId,
3010 ctor: CircleCtor,
3011 ) -> Result<(), KclError> {
3012 let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
3014 let new_center_ast = to_ast_point2d(&ctor.center).map_err(|err| KclError::refactor(err.to_string()))?;
3015
3016 let sketch_id = sketch;
3018 let sketch_object = self
3019 .scene_graph
3020 .objects
3021 .get(sketch_id.0)
3022 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
3023 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
3024 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
3025 };
3026 sketch.segments.iter().find(|o| **o == circle).ok_or_else(|| {
3027 KclError::refactor(format!(
3028 "Circle not found in sketch: circle={circle:?}, sketch={sketch:?}"
3029 ))
3030 })?;
3031 let circle_id = circle;
3033 let circle_object = self
3034 .scene_graph
3035 .objects
3036 .get(circle_id.0)
3037 .ok_or_else(|| KclError::refactor(format!("Circle not found in scene graph: circle={circle:?}")))?;
3038 let ObjectKind::Segment { .. } = &circle_object.kind else {
3039 return Err(KclError::refactor(format!(
3040 "Object is not a segment: {circle_object:?}"
3041 )));
3042 };
3043
3044 self.mutate_ast(
3046 new_ast,
3047 circle_id,
3048 AstMutateCommand::EditCircle {
3049 start: new_start_ast,
3050 center: new_center_ast,
3051 construction: ctor.construction,
3052 },
3053 )?;
3054 Ok(())
3055 }
3056
3057 fn edit_control_point_spline(
3058 &mut self,
3059 new_ast: &mut ast::Node<ast::Program>,
3060 sketch: ObjectId,
3061 spline: ObjectId,
3062 ctor: ControlPointSplineCtor,
3063 ) -> Result<(), KclError> {
3064 let points_ast = to_ast_point2d_array(&ctor.points).map_err(|err| KclError::refactor(err.to_string()))?;
3065
3066 let sketch_object = self
3067 .scene_graph
3068 .objects
3069 .get(sketch.0)
3070 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
3071 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
3072 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
3073 };
3074 sketch.segments.iter().find(|o| **o == spline).ok_or_else(|| {
3075 KclError::refactor(format!(
3076 "Control point spline not found in sketch: spline={spline:?}, sketch={sketch:?}"
3077 ))
3078 })?;
3079
3080 let spline_object =
3081 self.scene_graph.objects.get(spline.0).ok_or_else(|| {
3082 KclError::refactor(format!("Control point spline not found in scene graph: {spline:?}"))
3083 })?;
3084 let ObjectKind::Segment { .. } = &spline_object.kind else {
3085 return Err(KclError::refactor(format!(
3086 "Object is not a segment: {spline_object:?}"
3087 )));
3088 };
3089
3090 self.mutate_ast(
3091 new_ast,
3092 spline,
3093 AstMutateCommand::EditControlPointSpline {
3094 points: points_ast,
3095 construction: ctor.construction,
3096 },
3097 )?;
3098 Ok(())
3099 }
3100
3101 fn delete_segment(
3102 &mut self,
3103 new_ast: &mut ast::Node<ast::Program>,
3104 sketch: ObjectId,
3105 segment_id: ObjectId,
3106 ) -> Result<(), KclError> {
3107 let sketch_id = sketch;
3109 let sketch_object = self
3110 .scene_graph
3111 .objects
3112 .get(sketch_id.0)
3113 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
3114 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
3115 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
3116 };
3117 sketch.segments.iter().find(|o| **o == segment_id).ok_or_else(|| {
3118 KclError::refactor(format!(
3119 "Segment not found in sketch: segment={segment_id:?}, sketch={sketch:?}"
3120 ))
3121 })?;
3122 let segment_object =
3124 self.scene_graph.objects.get(segment_id.0).ok_or_else(|| {
3125 KclError::refactor(format!("Segment not found in scene graph: segment={segment_id:?}"))
3126 })?;
3127 let ObjectKind::Segment { .. } = &segment_object.kind else {
3128 return Err(KclError::refactor(format!(
3129 "Object is not a segment, it is {}",
3130 segment_object.kind.human_friendly_kind_with_article()
3131 )));
3132 };
3133
3134 self.mutate_ast(new_ast, segment_id, AstMutateCommand::DeleteNode)?;
3136 Ok(())
3137 }
3138
3139 fn delete_constraint(
3140 &mut self,
3141 new_ast: &mut ast::Node<ast::Program>,
3142 sketch: ObjectId,
3143 constraint_id: ObjectId,
3144 ) -> Result<(), KclError> {
3145 let sketch_id = sketch;
3147 let sketch_object = self
3148 .scene_graph
3149 .objects
3150 .get(sketch_id.0)
3151 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
3152 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
3153 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
3154 };
3155 sketch
3156 .constraints
3157 .iter()
3158 .find(|o| **o == constraint_id)
3159 .ok_or_else(|| {
3160 KclError::refactor(format!(
3161 "Constraint not found in sketch: constraint={constraint_id:?}, sketch={sketch:?}"
3162 ))
3163 })?;
3164 let constraint_object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
3166 KclError::refactor(format!(
3167 "Constraint not found in scene graph: constraint={constraint_id:?}"
3168 ))
3169 })?;
3170 let ObjectKind::Constraint { .. } = &constraint_object.kind else {
3171 return Err(KclError::refactor(format!(
3172 "Object is not a constraint, it is {}",
3173 constraint_object.kind.human_friendly_kind_with_article()
3174 )));
3175 };
3176
3177 self.mutate_ast(new_ast, constraint_id, AstMutateCommand::DeleteNode)?;
3179 Ok(())
3180 }
3181
3182 fn edit_coincident_constraint(
3183 &mut self,
3184 new_ast: &mut ast::Node<ast::Program>,
3185 constraint_id: ObjectId,
3186 segments: Vec<ConstraintSegment>,
3187 ) -> Result<(), KclError> {
3188 if segments.len() < 2 {
3189 return Err(KclError::refactor(format!(
3190 "Coincident constraint must have at least 2 inputs, got {}",
3191 segments.len()
3192 )));
3193 }
3194
3195 let segment_asts = segments
3196 .iter()
3197 .map(|segment| self.coincident_segment_to_ast(segment, new_ast))
3198 .collect::<Result<Vec<_>, _>>()?;
3199
3200 let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
3201 elements: segment_asts,
3202 digest: None,
3203 non_code_meta: Default::default(),
3204 })));
3205
3206 self.mutate_ast(
3207 new_ast,
3208 constraint_id,
3209 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3210 )?;
3211 Ok(())
3212 }
3213
3214 fn edit_horizontal_points_constraint(
3215 &mut self,
3216 new_ast: &mut ast::Node<ast::Program>,
3217 constraint_id: ObjectId,
3218 points: Vec<ConstraintSegment>,
3219 ) -> Result<(), KclError> {
3220 self.edit_axis_points_constraint(new_ast, constraint_id, points, "Horizontal")
3221 }
3222
3223 fn edit_vertical_points_constraint(
3224 &mut self,
3225 new_ast: &mut ast::Node<ast::Program>,
3226 constraint_id: ObjectId,
3227 points: Vec<ConstraintSegment>,
3228 ) -> Result<(), KclError> {
3229 self.edit_axis_points_constraint(new_ast, constraint_id, points, "Vertical")
3230 }
3231
3232 fn edit_axis_points_constraint(
3233 &mut self,
3234 new_ast: &mut ast::Node<ast::Program>,
3235 constraint_id: ObjectId,
3236 points: Vec<ConstraintSegment>,
3237 constraint_name: &str,
3238 ) -> Result<(), KclError> {
3239 if points.len() < 2 {
3240 return Err(KclError::refactor(format!(
3241 "{constraint_name} points constraint must have at least 2 points, got {}",
3242 points.len()
3243 )));
3244 }
3245
3246 let point_asts = points
3247 .iter()
3248 .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
3249 .collect::<Result<Vec<_>, _>>()?;
3250
3251 let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
3252 elements: point_asts,
3253 digest: None,
3254 non_code_meta: Default::default(),
3255 })));
3256
3257 self.mutate_ast(
3258 new_ast,
3259 constraint_id,
3260 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3261 )?;
3262 Ok(())
3263 }
3264
3265 fn edit_equal_length_constraint(
3267 &mut self,
3268 new_ast: &mut ast::Node<ast::Program>,
3269 constraint_id: ObjectId,
3270 lines: Vec<ObjectId>,
3271 ) -> Result<(), KclError> {
3272 if lines.len() < 2 {
3273 return Err(KclError::refactor(format!(
3274 "Lines equal length constraint must have at least 2 lines, got {}",
3275 lines.len()
3276 )));
3277 }
3278
3279 let line_asts = lines
3280 .iter()
3281 .map(|line_id| {
3282 let line_object = self
3283 .scene_graph
3284 .objects
3285 .get(line_id.0)
3286 .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3287 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3288 let kind = line_object.kind.human_friendly_kind_with_article();
3289 return Err(KclError::refactor(format!(
3290 "This constraint only works on Segments, but you selected {kind}"
3291 )));
3292 };
3293 let Segment::Line(_) = line_segment else {
3294 let kind = line_segment.human_friendly_kind_with_article();
3295 return Err(KclError::refactor(format!(
3296 "Only lines can be made equal length, but you selected {kind}"
3297 )));
3298 };
3299
3300 get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)
3301 })
3302 .collect::<Result<Vec<_>, _>>()?;
3303
3304 let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
3305 elements: line_asts,
3306 digest: None,
3307 non_code_meta: Default::default(),
3308 })));
3309
3310 self.mutate_ast(
3311 new_ast,
3312 constraint_id,
3313 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3314 )?;
3315 Ok(())
3316 }
3317
3318 fn edit_parallel_constraint(
3320 &mut self,
3321 new_ast: &mut ast::Node<ast::Program>,
3322 constraint_id: ObjectId,
3323 lines: Vec<ObjectId>,
3324 ) -> Result<(), KclError> {
3325 if lines.len() < 2 {
3326 return Err(KclError::refactor(format!(
3327 "Parallel constraint must have at least 2 lines, got {}",
3328 lines.len()
3329 )));
3330 }
3331
3332 let line_asts = lines
3333 .iter()
3334 .map(|line_id| {
3335 let line_object = self
3336 .scene_graph
3337 .objects
3338 .get(line_id.0)
3339 .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3340 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3341 let kind = line_object.kind.human_friendly_kind_with_article();
3342 return Err(KclError::refactor(format!(
3343 "This constraint only works on Segments, but you selected {kind}"
3344 )));
3345 };
3346 let Segment::Line(_) = line_segment else {
3347 let kind = line_segment.human_friendly_kind_with_article();
3348 return Err(KclError::refactor(format!(
3349 "Only lines can be made parallel, but you selected {kind}"
3350 )));
3351 };
3352
3353 get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)
3354 })
3355 .collect::<Result<Vec<_>, _>>()?;
3356
3357 let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
3358 elements: line_asts,
3359 digest: None,
3360 non_code_meta: Default::default(),
3361 })));
3362
3363 self.mutate_ast(
3364 new_ast,
3365 constraint_id,
3366 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3367 )?;
3368 Ok(())
3369 }
3370
3371 fn edit_equal_radius_constraint(
3373 &mut self,
3374 new_ast: &mut ast::Node<ast::Program>,
3375 constraint_id: ObjectId,
3376 input: Vec<ObjectId>,
3377 ) -> Result<(), KclError> {
3378 if input.len() < 2 {
3379 return Err(KclError::refactor(format!(
3380 "equalRadius constraint must have at least 2 segments, got {}",
3381 input.len()
3382 )));
3383 }
3384
3385 let input_asts = input
3386 .iter()
3387 .map(|segment_id| self.equal_radius_segment_id_to_ast_reference(*segment_id, new_ast))
3388 .collect::<Result<Vec<_>, _>>()?;
3389
3390 let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
3391 elements: input_asts,
3392 digest: None,
3393 non_code_meta: Default::default(),
3394 })));
3395
3396 self.mutate_ast(
3397 new_ast,
3398 constraint_id,
3399 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3400 )?;
3401 Ok(())
3402 }
3403
3404 async fn execute_after_edit(
3405 &mut self,
3406 ctx: &ExecutorContext,
3407 sketch: ObjectId,
3408 sketch_block_ref: AstNodeRef,
3409 new_ast: &mut ast::Node<ast::Program>,
3410 options: ExecuteAfterEditOptions,
3411 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
3412 let ExecuteAfterEditOptions {
3413 segment_ids_edited,
3414 edit_kind,
3415 commit_solved_initial_guesses,
3416 } = options;
3417
3418 let new_source = source_from_ast(new_ast);
3420 let new_program = parse_frontend_mutation_source(
3422 &new_source,
3423 "Error parsing KCL source after editing",
3424 "No AST produced after editing",
3425 )?;
3426
3427 let is_delete = edit_kind.is_delete();
3429 let truncated_program = {
3430 let mut truncated_program = new_program.clone();
3431 only_sketch_block(
3432 &mut truncated_program.ast,
3433 &sketch_block_ref,
3434 edit_kind.to_change_kind(),
3435 )
3436 .map_err(KclErrorWithOutputs::no_outputs)?;
3437 truncated_program
3438 };
3439
3440 let drag_anchors = self.next_segment_drag_anchors.take().unwrap_or_default();
3442 let mock_config = MockConfig {
3443 sketch_block_id: Some(sketch),
3444 freedom_analysis: is_delete,
3445 segment_ids_edited: segment_ids_edited.clone(),
3446 drag_anchors,
3447 ..Default::default()
3448 };
3449 let outcome = ctx.run_mock(&truncated_program, &mock_config).await?;
3450
3451 self.program = new_program;
3453
3454 let outcome = self.update_state_after_exec(outcome, is_delete);
3456
3457 let src_delta = if commit_solved_initial_guesses {
3458 self.commit_var_solutions_to_program(&outcome, "editing")?
3459 } else {
3460 SourceDelta { text: new_source }
3461 };
3462 let scene_graph_delta = SceneGraphDelta {
3463 new_graph: self.scene_graph_for_ui(),
3464 invalidates_ids: is_delete,
3465 new_objects: Vec::new(),
3466 exec_outcome: outcome,
3467 };
3468 Ok((src_delta, scene_graph_delta))
3469 }
3470
3471 async fn execute_after_delete_sketch(
3472 &mut self,
3473 ctx: &ExecutorContext,
3474 new_ast: &mut ast::Node<ast::Program>,
3475 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
3476 let new_source = source_from_ast(new_ast);
3478 let new_program = parse_frontend_mutation_source(
3480 &new_source,
3481 "Error parsing KCL source after editing",
3482 "No AST produced after editing",
3483 )?;
3484
3485 self.program = new_program.clone();
3487
3488 let outcome = ctx.run_with_caching(new_program).await?;
3494 let freedom_analysis_ran = true;
3495
3496 let outcome = self.update_state_after_exec(outcome, freedom_analysis_ran);
3497
3498 let src_delta = SourceDelta { text: new_source };
3499 let scene_graph_delta = SceneGraphDelta {
3500 new_graph: self.scene_graph_for_ui(),
3501 invalidates_ids: true,
3502 new_objects: Vec::new(),
3503 exec_outcome: outcome,
3504 };
3505 Ok((src_delta, scene_graph_delta))
3506 }
3507
3508 fn point_id_to_ast_reference(
3513 &self,
3514 point_id: ObjectId,
3515 new_ast: &mut ast::Node<ast::Program>,
3516 ) -> Result<ast::Expr, KclError> {
3517 let point_object = self
3518 .scene_graph
3519 .objects
3520 .get(point_id.0)
3521 .ok_or_else(|| KclError::refactor(format!("Point not found: {point_id:?}")))?;
3522 let ObjectKind::Segment { segment: point_segment } = &point_object.kind else {
3523 return Err(KclError::refactor(format!("Object is not a segment: {point_object:?}")));
3524 };
3525 let Segment::Point(point) = point_segment else {
3526 return Err(KclError::refactor(format!(
3527 "Only points are currently supported: {point_object:?}"
3528 )));
3529 };
3530
3531 if let Some(owner_id) = point.owner {
3532 let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
3533 KclError::refactor(format!(
3534 "Owner of point not found in scene graph: point={point_id:?}, owner={owner_id:?}"
3535 ))
3536 })?;
3537 let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind else {
3538 return Err(KclError::refactor(format!(
3539 "Owner of point is not a segment, but found {}",
3540 owner_object.kind.human_friendly_kind_with_article()
3541 )));
3542 };
3543
3544 match owner_segment {
3545 Segment::Line(line) => {
3546 let property = if line.start == point_id {
3547 LINE_PROPERTY_START
3548 } else if line.end == point_id {
3549 LINE_PROPERTY_END
3550 } else {
3551 return Err(KclError::refactor(format!(
3552 "Internal: Point is not part of owner's line segment: point={point_id:?}, line={owner_id:?}"
3553 )));
3554 };
3555 get_or_insert_ast_reference(new_ast, &owner_object.source, LINE_VARIABLE, Some(property))
3556 }
3557 Segment::Arc(arc) => {
3558 let property = if arc.start == point_id {
3559 ARC_PROPERTY_START
3560 } else if arc.end == point_id {
3561 ARC_PROPERTY_END
3562 } else if arc.center == point_id {
3563 ARC_PROPERTY_CENTER
3564 } else {
3565 return Err(KclError::refactor(format!(
3566 "Internal: Point is not part of owner's arc segment: point={point_id:?}, arc={owner_id:?}"
3567 )));
3568 };
3569 get_or_insert_ast_reference(new_ast, &owner_object.source, ARC_VARIABLE, Some(property))
3570 }
3571 Segment::Circle(circle) => {
3572 let property = if circle.start == point_id {
3573 CIRCLE_PROPERTY_START
3574 } else if circle.center == point_id {
3575 CIRCLE_PROPERTY_CENTER
3576 } else {
3577 return Err(KclError::refactor(format!(
3578 "Internal: Point is not part of owner's circle segment: point={point_id:?}, circle={owner_id:?}"
3579 )));
3580 };
3581 get_or_insert_ast_reference(new_ast, &owner_object.source, CIRCLE_VARIABLE, Some(property))
3582 }
3583 Segment::ControlPointSpline(spline) => {
3584 let Some(index) = spline.controls.iter().position(|id| *id == point_id) else {
3585 return Err(KclError::refactor(format!(
3586 "Internal: Point is not part of owner's controlPointSpline segment: point={point_id:?}, spline={owner_id:?}"
3587 )));
3588 };
3589 let owner_expr =
3590 get_or_insert_ast_reference(new_ast, &owner_object.source, CONTROL_POINT_SPLINE_FN, None)?;
3591 let controls_expr = create_member_expression(owner_expr, CONTROL_POINT_SPLINE_PROPERTY_CONTROLS);
3592 Ok(create_index_expression(controls_expr, index))
3593 }
3594 _ => Err(KclError::refactor(format!(
3595 "Internal: Owner of point is not a supported segment type for constraints: {owner_segment:?}"
3596 ))),
3597 }
3598 } else {
3599 get_or_insert_ast_reference(new_ast, &point_object.source, "point", None)
3601 }
3602 }
3603
3604 fn line_id_to_ast_reference(
3605 &self,
3606 line_id: ObjectId,
3607 new_ast: &mut ast::Node<ast::Program>,
3608 ) -> Result<ast::Expr, KclError> {
3609 let line_object = self
3610 .scene_graph
3611 .objects
3612 .get(line_id.0)
3613 .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3614 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3615 return Err(KclError::refactor(format!("Object is not a segment: {line_object:?}")));
3616 };
3617 let Segment::Line(line) = line_segment else {
3618 return Err(KclError::refactor(format!(
3619 "Only lines are currently supported: {line_object:?}"
3620 )));
3621 };
3622
3623 if let Some(owner_id) = line.owner {
3624 let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
3625 KclError::refactor(format!(
3626 "Owner of line not found in scene graph: line={line_id:?}, owner={owner_id:?}"
3627 ))
3628 })?;
3629 let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind else {
3630 return Err(KclError::refactor(format!(
3631 "Owner of line is not a segment, but found {}",
3632 owner_object.kind.human_friendly_kind_with_article()
3633 )));
3634 };
3635
3636 match owner_segment {
3637 Segment::ControlPointSpline(spline) => {
3638 let Some(index) = spline
3639 .controls
3640 .windows(2)
3641 .position(|window| window[0] == line.start && window[1] == line.end)
3642 else {
3643 return Err(KclError::refactor(format!(
3644 "Internal: Line is not part of owner's controlPointSpline segment: line={line_id:?}, spline={owner_id:?}"
3645 )));
3646 };
3647 let owner_expr =
3648 get_or_insert_ast_reference(new_ast, &owner_object.source, CONTROL_POINT_SPLINE_FN, None)?;
3649 let edges_expr = create_member_expression(owner_expr, CONTROL_POINT_SPLINE_PROPERTY_EDGES);
3650 Ok(create_index_expression(edges_expr, index))
3651 }
3652 _ => Err(KclError::refactor(format!(
3653 "Internal: Owner of line is not a supported segment type for constraints: {owner_segment:?}"
3654 ))),
3655 }
3656 } else {
3657 get_or_insert_ast_reference(new_ast, &line_object.source, "line", None)
3658 }
3659 }
3660
3661 fn coincident_segment_to_ast(
3662 &self,
3663 segment: &ConstraintSegment,
3664 new_ast: &mut ast::Node<ast::Program>,
3665 ) -> Result<ast::Expr, KclError> {
3666 match segment {
3667 ConstraintSegment::Origin(_) => Ok(ast_name_expr("ORIGIN".to_owned())),
3668 ConstraintSegment::Segment(segment_id) => self.segment_id_to_constraint_ast_reference(*segment_id, new_ast),
3669 }
3670 }
3671
3672 fn segment_id_to_constraint_ast_reference(
3673 &self,
3674 segment_id: ObjectId,
3675 new_ast: &mut ast::Node<ast::Program>,
3676 ) -> Result<ast::Expr, KclError> {
3677 let segment_object = self
3678 .scene_graph
3679 .objects
3680 .get(segment_id.0)
3681 .ok_or_else(|| KclError::refactor(format!("Object not found: {segment_id:?}")))?;
3682 let ObjectKind::Segment { segment } = &segment_object.kind else {
3683 return Err(KclError::refactor(format!(
3684 "Object is not a segment, it is {}",
3685 segment_object.kind.human_friendly_kind_with_article()
3686 )));
3687 };
3688
3689 match segment {
3690 Segment::Point(_) => self.point_id_to_ast_reference(segment_id, new_ast),
3691 Segment::Line(_) => self.line_id_to_ast_reference(segment_id, new_ast),
3692 Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, "arc", None),
3693 Segment::Circle(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, CIRCLE_VARIABLE, None),
3694 Segment::ControlPointSpline(_) => {
3695 get_or_insert_ast_reference(new_ast, &segment_object.source, CONTROL_POINT_SPLINE_FN, None)
3696 }
3697 }
3698 }
3699
3700 fn axis_constraint_segment_to_ast(
3701 &self,
3702 segment: &ConstraintSegment,
3703 new_ast: &mut ast::Node<ast::Program>,
3704 ) -> Result<ast::Expr, KclError> {
3705 match segment {
3706 ConstraintSegment::Origin(_) => Ok(ast_name_expr("ORIGIN".to_owned())),
3707 ConstraintSegment::Segment(point_id) => self.point_id_to_ast_reference(*point_id, new_ast),
3708 }
3709 }
3710
3711 async fn add_coincident(
3712 &mut self,
3713 sketch: ObjectId,
3714 coincident: Coincident,
3715 new_ast: &mut ast::Node<ast::Program>,
3716 ) -> Result<AstNodeRef, KclError> {
3717 let sketch_id = sketch;
3718 for segment in &coincident.segments {
3719 let ConstraintSegment::Segment(segment_id) = segment else {
3720 continue;
3721 };
3722 let Some(segment_object) = self.scene_graph.objects.get(segment_id.0) else {
3723 continue;
3724 };
3725 if matches!(
3726 segment_object.kind,
3727 ObjectKind::Segment {
3728 segment: Segment::ControlPointSpline(_)
3729 }
3730 ) {
3731 return Err(KclError::refactor(
3732 "Coincident with a full controlPointSpline is not supported yet. Constrain a control point or spline edge instead."
3733 .to_owned(),
3734 ));
3735 }
3736 }
3737 let segment_asts = coincident
3738 .segments
3739 .iter()
3740 .map(|segment| self.coincident_segment_to_ast(segment, new_ast))
3741 .collect::<Result<Vec<_>, _>>()?;
3742 if segment_asts.len() < 2 {
3743 return Err(KclError::refactor(format!(
3744 "Coincident constraint must have at least 2 inputs, got {}",
3745 segment_asts.len()
3746 )));
3747 }
3748
3749 let coincident_ast = create_coincident_ast(segment_asts);
3751
3752 let (sketch_block_ref, _) = self.mutate_ast(
3754 new_ast,
3755 sketch_id,
3756 AstMutateCommand::AddSketchBlockExprStmt { expr: coincident_ast },
3757 )?;
3758 Ok(sketch_block_ref)
3759 }
3760
3761 async fn add_distance(
3762 &mut self,
3763 sketch: ObjectId,
3764 distance: Distance,
3765 new_ast: &mut ast::Node<ast::Program>,
3766 ) -> Result<AstNodeRef, KclError> {
3767 self.add_distance_constraint(sketch, DISTANCE_FN, distance, new_ast)
3768 }
3769
3770 fn distance_constraint_ast_parts(
3771 &self,
3772 function_name: &str,
3773 distance: &Distance,
3774 new_ast: &mut ast::Node<ast::Program>,
3775 ) -> Result<(ast::BinaryPart, ast::BinaryPart), KclError> {
3776 let [segment0_ast, segment1_ast] = match distance.segments.as_slice() {
3777 [pt0, pt1] => [
3778 self.coincident_segment_to_ast(pt0, new_ast)?,
3779 self.coincident_segment_to_ast(pt1, new_ast)?,
3780 ],
3781 _ => {
3782 return Err(KclError::refactor(format!(
3783 "Distance constraint must have exactly 2 segments, got {}",
3784 distance.segments.len()
3785 )));
3786 }
3787 };
3788
3789 let arguments = match &distance.label_position {
3790 Some(label_position) => vec![ast::LabeledArg {
3791 label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
3792 arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
3793 }],
3794 None => Default::default(),
3795 };
3796
3797 let call = ast::BinaryPart::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
3798 callee: ast::Node::no_src(ast_sketch2_name(function_name)),
3799 unlabeled: Some(ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(
3800 ast::ArrayExpression {
3801 elements: vec![segment0_ast, segment1_ast],
3802 digest: None,
3803 non_code_meta: Default::default(),
3804 },
3805 )))),
3806 arguments,
3807 digest: None,
3808 non_code_meta: Default::default(),
3809 })));
3810 let value = ast::BinaryPart::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
3811 value: ast::LiteralValue::Number {
3812 value: distance.distance.value,
3813 suffix: distance.distance.units,
3814 },
3815 raw: format_number_literal(distance.distance.value, distance.distance.units, None).map_err(|_| {
3816 KclError::refactor(format!(
3817 "Could not format numeric suffix: {:?}",
3818 distance.distance.units
3819 ))
3820 })?,
3821 digest: None,
3822 })));
3823
3824 Ok((call, value))
3825 }
3826
3827 fn add_distance_constraint(
3828 &mut self,
3829 sketch: ObjectId,
3830 function_name: &str,
3831 distance: Distance,
3832 new_ast: &mut ast::Node<ast::Program>,
3833 ) -> Result<AstNodeRef, KclError> {
3834 let (call, value) = self.distance_constraint_ast_parts(function_name, &distance, new_ast)?;
3835 let distance_ast = ast::Expr::BinaryExpression(BoxNode::new(ast::Node::no_src(ast::BinaryExpression {
3836 left: call,
3837 operator: ast::BinaryOperator::Eq,
3838 right: value,
3839 digest: None,
3840 })));
3841
3842 let (sketch_block_ref, _) = self.mutate_ast(
3843 new_ast,
3844 sketch,
3845 AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
3846 )?;
3847 Ok(sketch_block_ref)
3848 }
3849
3850 async fn add_angle(
3851 &mut self,
3852 sketch: ObjectId,
3853 angle: Angle,
3854 new_ast: &mut ast::Node<ast::Program>,
3855 ) -> Result<AstNodeRef, KclError> {
3856 let sketch_id = sketch;
3857 let (angle_call_ast, angle_value_ast) = self.angle_constraint_ast_parts(&angle, new_ast)?;
3858 let angle_ast = ast::Expr::BinaryExpression(BoxNode::new(ast::Node::no_src(ast::BinaryExpression {
3859 left: angle_call_ast,
3860 operator: ast::BinaryOperator::Eq,
3861 right: angle_value_ast,
3862 digest: None,
3863 })));
3864
3865 let (sketch_block_ref, _) = self.mutate_ast(
3867 new_ast,
3868 sketch_id,
3869 AstMutateCommand::AddSketchBlockExprStmt { expr: angle_ast },
3870 )?;
3871 Ok(sketch_block_ref)
3872 }
3873
3874 fn angle_constraint_ast_parts(
3875 &self,
3876 angle: &Angle,
3877 new_ast: &mut ast::Node<ast::Program>,
3878 ) -> Result<(ast::BinaryPart, ast::BinaryPart), KclError> {
3879 let &[l0_id, l1_id] = angle.lines.as_slice() else {
3880 return Err(KclError::refactor(format!(
3881 "Angle constraint must have exactly 2 lines, got {}",
3882 angle.lines.len()
3883 )));
3884 };
3885
3886 let l0_ast = self.line_id_to_ast_reference(l0_id, new_ast)?;
3887 let l1_ast = self.line_id_to_ast_reference(l1_id, new_ast)?;
3888 let lines_ast = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
3889 elements: vec![l0_ast, l1_ast],
3890 digest: None,
3891 non_code_meta: Default::default(),
3892 })));
3893
3894 if angle.inverse == Some(true) && angle.sector.is_none() {
3895 return Err(KclError::refactor("Angle inverse requires an angle sector".to_owned()));
3896 }
3897
3898 let uses_angle_dimension = angle.sector.is_some();
3899 let mut arguments = if uses_angle_dimension {
3900 vec![ast::LabeledArg {
3901 label: Some(ast::Identifier::new(ANGLE_LINES_PARAM)),
3902 arg: lines_ast.clone(),
3903 }]
3904 } else {
3905 Default::default()
3906 };
3907
3908 if let Some(sector) = angle.sector {
3909 arguments.push(ast::LabeledArg {
3910 label: Some(ast::Identifier::new(ANGLE_SECTOR_PARAM)),
3911 arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
3912 value: ast::LiteralValue::Number {
3913 value: f64::from(sector),
3914 suffix: NumericSuffix::None,
3915 },
3916 raw: sector.to_string(),
3917 digest: None,
3918 }))),
3919 });
3920 }
3921
3922 if angle.inverse == Some(true) {
3923 arguments.push(ast::LabeledArg {
3924 label: Some(ast::Identifier::new(ANGLE_INVERSE_PARAM)),
3925 arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
3926 value: ast::LiteralValue::Bool(true),
3927 raw: true.to_string(),
3928 digest: None,
3929 }))),
3930 });
3931 }
3932
3933 if let Some(label_position) = &angle.label_position {
3934 arguments.push(ast::LabeledArg {
3935 label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
3936 arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
3937 });
3938 }
3939
3940 let call = ast::BinaryPart::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
3941 callee: ast::Node::no_src(ast_sketch2_name(if uses_angle_dimension {
3942 ANGLE_DIMENSION_FN
3943 } else {
3944 ANGLE_FN
3945 })),
3946 unlabeled: (!uses_angle_dimension).then_some(lines_ast),
3947 arguments,
3948 digest: None,
3949 non_code_meta: Default::default(),
3950 })));
3951 let value = ast::BinaryPart::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
3952 value: ast::LiteralValue::Number {
3953 value: angle.angle.value,
3954 suffix: angle.angle.units,
3955 },
3956 raw: format_number_literal(angle.angle.value, angle.angle.units, None)
3957 .map_err(|_| KclError::refactor(format!("Could not format numeric suffix: {:?}", angle.angle.units)))?,
3958 digest: None,
3959 })));
3960
3961 Ok((call, value))
3962 }
3963
3964 async fn add_tangent(
3965 &mut self,
3966 sketch: ObjectId,
3967 tangent: Tangent,
3968 new_ast: &mut ast::Node<ast::Program>,
3969 ) -> Result<AstNodeRef, KclError> {
3970 let &[seg0_id, seg1_id] = tangent.input.as_slice() else {
3971 return Err(KclError::refactor(format!(
3972 "Tangent constraint must have exactly 2 segments, got {}",
3973 tangent.input.len()
3974 )));
3975 };
3976 let sketch_id = sketch;
3977
3978 let seg0_object = self
3979 .scene_graph
3980 .objects
3981 .get(seg0_id.0)
3982 .ok_or_else(|| KclError::refactor(format!("Segment not found: {seg0_id:?}")))?;
3983 let ObjectKind::Segment { segment: seg0_segment } = &seg0_object.kind else {
3984 return Err(KclError::refactor(format!("Object is not a segment: {seg0_object:?}")));
3985 };
3986 let seg0_ast = match seg0_segment {
3987 Segment::Line(_) | Segment::Arc(_) | Segment::Circle(_) => {
3988 self.segment_id_to_constraint_ast_reference(seg0_id, new_ast)?
3989 }
3990 _ => {
3991 return Err(KclError::refactor(format!(
3992 "Tangent supports only line/arc/circle segments for now, got: {seg0_segment:?}"
3993 )));
3994 }
3995 };
3996
3997 let seg1_object = self
3998 .scene_graph
3999 .objects
4000 .get(seg1_id.0)
4001 .ok_or_else(|| KclError::refactor(format!("Segment not found: {seg1_id:?}")))?;
4002 let ObjectKind::Segment { segment: seg1_segment } = &seg1_object.kind else {
4003 return Err(KclError::refactor(format!("Object is not a segment: {seg1_object:?}")));
4004 };
4005 let seg1_ast = match seg1_segment {
4006 Segment::Line(_) | Segment::Arc(_) | Segment::Circle(_) => {
4007 self.segment_id_to_constraint_ast_reference(seg1_id, new_ast)?
4008 }
4009 _ => {
4010 return Err(KclError::refactor(format!(
4011 "Tangent supports only line/arc/circle segments for now, got: {seg1_segment:?}"
4012 )));
4013 }
4014 };
4015
4016 let tangent_ast = create_tangent_ast(seg0_ast, seg1_ast);
4017 let (sketch_block_ref, _) = self.mutate_ast(
4018 new_ast,
4019 sketch_id,
4020 AstMutateCommand::AddSketchBlockExprStmt { expr: tangent_ast },
4021 )?;
4022 Ok(sketch_block_ref)
4023 }
4024
4025 async fn add_symmetric(
4026 &mut self,
4027 sketch: ObjectId,
4028 symmetric: Symmetric,
4029 new_ast: &mut ast::Node<ast::Program>,
4030 ) -> Result<AstNodeRef, KclError> {
4031 let &[input0_id, input1_id] = symmetric.input.as_slice() else {
4032 return Err(KclError::refactor(format!(
4033 "Symmetric constraint must have exactly 2 inputs, got {}",
4034 symmetric.input.len()
4035 )));
4036 };
4037 let sketch_id = sketch;
4038
4039 let input0_ast = self.symmetric_input_id_to_ast_reference(input0_id, new_ast)?;
4040 let input1_ast = self.symmetric_input_id_to_ast_reference(input1_id, new_ast)?;
4041 let axis_ast = self.symmetric_axis_id_to_ast_reference(symmetric.axis, new_ast)?;
4042
4043 let symmetric_ast = create_symmetric_ast(vec![input0_ast, input1_ast], axis_ast);
4044 let (sketch_block_ref, _) = self.mutate_ast(
4045 new_ast,
4046 sketch_id,
4047 AstMutateCommand::AddSketchBlockExprStmt { expr: symmetric_ast },
4048 )?;
4049 Ok(sketch_block_ref)
4050 }
4051
4052 async fn add_midpoint(
4053 &mut self,
4054 sketch: ObjectId,
4055 midpoint: Midpoint,
4056 new_ast: &mut ast::Node<ast::Program>,
4057 ) -> Result<AstNodeRef, KclError> {
4058 let sketch_id = sketch;
4059 let point_ast = self.axis_constraint_segment_to_ast(&midpoint.point, new_ast)?;
4060
4061 let segment_object = self
4062 .scene_graph
4063 .objects
4064 .get(midpoint.segment.0)
4065 .ok_or_else(|| KclError::refactor(format!("Segment not found: {:?}", midpoint.segment)))?;
4066 let ObjectKind::Segment {
4067 segment: midpoint_segment,
4068 } = &segment_object.kind
4069 else {
4070 return Err(KclError::refactor(format!(
4071 "Object must be a segment, but it was {}",
4072 segment_object.kind.human_friendly_kind_with_article()
4073 )));
4074 };
4075 let segment_ast = match midpoint_segment {
4076 Segment::Line(_) => self.line_id_to_ast_reference(midpoint.segment, new_ast)?,
4077 Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, "arc", None)?,
4078 _ => {
4079 return Err(KclError::refactor(format!(
4080 "Midpoint target must be a line or arc segment but it was {}",
4081 midpoint_segment.human_friendly_kind_with_article()
4082 )));
4083 }
4084 };
4085
4086 let midpoint_ast = create_midpoint_ast(segment_ast, point_ast);
4087 let (sketch_block_ref, _) = self.mutate_ast(
4088 new_ast,
4089 sketch_id,
4090 AstMutateCommand::AddSketchBlockExprStmt { expr: midpoint_ast },
4091 )?;
4092 Ok(sketch_block_ref)
4093 }
4094
4095 async fn add_equal_radius(
4096 &mut self,
4097 sketch: ObjectId,
4098 equal_radius: EqualRadius,
4099 new_ast: &mut ast::Node<ast::Program>,
4100 ) -> Result<AstNodeRef, KclError> {
4101 if equal_radius.input.len() < 2 {
4102 return Err(KclError::refactor(format!(
4103 "equalRadius constraint must have at least 2 segments, got {}",
4104 equal_radius.input.len()
4105 )));
4106 }
4107
4108 let sketch_id = sketch;
4109 let input_asts = equal_radius
4110 .input
4111 .iter()
4112 .map(|segment_id| self.equal_radius_segment_id_to_ast_reference(*segment_id, new_ast))
4113 .collect::<Result<Vec<_>, _>>()?;
4114
4115 let equal_radius_ast = create_equal_radius_ast(input_asts);
4116 let (sketch_block_ref, _) = self.mutate_ast(
4117 new_ast,
4118 sketch_id,
4119 AstMutateCommand::AddSketchBlockExprStmt { expr: equal_radius_ast },
4120 )?;
4121 Ok(sketch_block_ref)
4122 }
4123
4124 async fn add_radius(
4125 &mut self,
4126 sketch: ObjectId,
4127 radius: Radius,
4128 new_ast: &mut ast::Node<ast::Program>,
4129 ) -> Result<AstNodeRef, KclError> {
4130 let params = ArcSizeConstraintParams {
4131 points: vec![radius.arc],
4132 function_name: RADIUS_FN,
4133 value: radius.radius.value,
4134 units: radius.radius.units,
4135 label_position: radius.label_position,
4136 constraint_type_name: "Radius",
4137 };
4138 self.add_arc_size_constraint(sketch, params, new_ast).await
4139 }
4140
4141 async fn add_diameter(
4142 &mut self,
4143 sketch: ObjectId,
4144 diameter: Diameter,
4145 new_ast: &mut ast::Node<ast::Program>,
4146 ) -> Result<AstNodeRef, KclError> {
4147 let params = ArcSizeConstraintParams {
4148 points: vec![diameter.arc],
4149 function_name: DIAMETER_FN,
4150 value: diameter.diameter.value,
4151 units: diameter.diameter.units,
4152 label_position: diameter.label_position,
4153 constraint_type_name: "Diameter",
4154 };
4155 self.add_arc_size_constraint(sketch, params, new_ast).await
4156 }
4157
4158 async fn add_fixed_constraints(
4159 &mut self,
4160 sketch: ObjectId,
4161 points: Vec<FixedPoint>,
4162 new_ast: &mut ast::Node<ast::Program>,
4163 ) -> Result<AstNodeRef, KclError> {
4164 let mut sketch_block_ref = None;
4165
4166 for fixed_point in points {
4167 let point_ast = self.point_id_to_ast_reference(fixed_point.point, new_ast)?;
4168 let fixed_ast = create_fixed_point_constraint_ast(point_ast, fixed_point.position)
4169 .map_err(|err| KclError::refactor(err.to_string()))?;
4170
4171 let (sketch_ref, _) = self.mutate_ast(
4172 new_ast,
4173 sketch,
4174 AstMutateCommand::AddSketchBlockExprStmt { expr: fixed_ast },
4175 )?;
4176 sketch_block_ref = Some(sketch_ref);
4177 }
4178
4179 sketch_block_ref.ok_or_else(|| KclError::refactor("Fixed constraint requires at least one point".to_owned()))
4180 }
4181
4182 async fn add_arc_size_constraint(
4183 &mut self,
4184 sketch: ObjectId,
4185 params: ArcSizeConstraintParams,
4186 new_ast: &mut ast::Node<ast::Program>,
4187 ) -> Result<AstNodeRef, KclError> {
4188 let sketch_id = sketch;
4189
4190 if params.points.len() != 1 {
4192 return Err(KclError::refactor(format!(
4193 "{} constraint must have exactly 1 argument (an arc segment), got {}",
4194 params.constraint_type_name,
4195 params.points.len()
4196 )));
4197 }
4198
4199 let arc_id = params.points[0];
4200 let arc_object = self
4201 .scene_graph
4202 .objects
4203 .get(arc_id.0)
4204 .ok_or_else(|| KclError::refactor(format!("Arc segment not found: {arc_id:?}")))?;
4205 let ObjectKind::Segment { segment: arc_segment } = &arc_object.kind else {
4206 return Err(KclError::refactor(format!("Object is not a segment: {arc_object:?}")));
4207 };
4208 let ref_type = match arc_segment {
4209 Segment::Arc(_) => ARC_VARIABLE,
4210 Segment::Circle(_) => CIRCLE_VARIABLE,
4211 _ => {
4212 return Err(KclError::refactor(format!(
4213 "{} constraint argument must be an arc or circle segment, got: {arc_segment:?}",
4214 params.constraint_type_name
4215 )));
4216 }
4217 };
4218 let arc_ast = get_or_insert_ast_reference(new_ast, &arc_object.source, ref_type, None)?;
4220 let arguments = match ¶ms.label_position {
4221 Some(label_position) => vec![ast::LabeledArg {
4222 label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
4223 arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
4224 }],
4225 None => Default::default(),
4226 };
4227
4228 let call_ast = ast::BinaryPart::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
4230 callee: ast::Node::no_src(ast_sketch2_name(params.function_name)),
4231 unlabeled: Some(arc_ast),
4232 arguments,
4233 digest: None,
4234 non_code_meta: Default::default(),
4235 })));
4236 let constraint_ast = ast::Expr::BinaryExpression(BoxNode::new(ast::Node::no_src(ast::BinaryExpression {
4237 left: call_ast,
4238 operator: ast::BinaryOperator::Eq,
4239 right: ast::BinaryPart::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
4240 value: ast::LiteralValue::Number {
4241 value: params.value,
4242 suffix: params.units,
4243 },
4244 raw: format_number_literal(params.value, params.units, None)
4245 .map_err(|_| KclError::refactor(format!("Could not format numeric suffix: {:?}", params.units)))?,
4246 digest: None,
4247 }))),
4248 digest: None,
4249 })));
4250
4251 let (sketch_block_ref, _) = self.mutate_ast(
4253 new_ast,
4254 sketch_id,
4255 AstMutateCommand::AddSketchBlockExprStmt { expr: constraint_ast },
4256 )?;
4257 Ok(sketch_block_ref)
4258 }
4259
4260 async fn add_horizontal_distance(
4261 &mut self,
4262 sketch: ObjectId,
4263 distance: Distance,
4264 new_ast: &mut ast::Node<ast::Program>,
4265 ) -> Result<AstNodeRef, KclError> {
4266 self.add_distance_constraint(sketch, HORIZONTAL_DISTANCE_FN, distance, new_ast)
4267 }
4268
4269 async fn add_vertical_distance(
4270 &mut self,
4271 sketch: ObjectId,
4272 distance: Distance,
4273 new_ast: &mut ast::Node<ast::Program>,
4274 ) -> Result<AstNodeRef, KclError> {
4275 self.add_distance_constraint(sketch, VERTICAL_DISTANCE_FN, distance, new_ast)
4276 }
4277
4278 async fn add_horizontal(
4279 &mut self,
4280 sketch: ObjectId,
4281 horizontal: Horizontal,
4282 new_ast: &mut ast::Node<ast::Program>,
4283 ) -> Result<AstNodeRef, KclError> {
4284 let sketch_id = sketch;
4285
4286 let first_arg_ast = match horizontal {
4288 Horizontal::Line { line } => {
4289 let line_object = self
4290 .scene_graph
4291 .objects
4292 .get(line.0)
4293 .ok_or_else(|| KclError::refactor(format!("Line not found: {line:?}")))?;
4294 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4295 let kind = line_object.kind.human_friendly_kind_with_article();
4296 return Err(KclError::refactor(format!(
4297 "This constraint only works on Segments, but you selected {kind}"
4298 )));
4299 };
4300 let Segment::Line(_) = line_segment else {
4301 return Err(KclError::refactor(format!(
4302 "Only lines can be made horizontal, but you selected {}",
4303 line_segment.human_friendly_kind_with_article(),
4304 )));
4305 };
4306 self.line_id_to_ast_reference(line, new_ast)?
4307 }
4308 Horizontal::Points { points } => {
4309 let point_asts = points
4310 .iter()
4311 .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
4312 .collect::<Result<Vec<_>, _>>()?;
4313 ast::ArrayExpression::new(point_asts).into()
4314 }
4315 };
4316 let horizontal_ast = create_horizontal_ast(first_arg_ast);
4318
4319 let (sketch_block_ref, _) = self.mutate_ast(
4321 new_ast,
4322 sketch_id,
4323 AstMutateCommand::AddSketchBlockExprStmt { expr: horizontal_ast },
4324 )?;
4325 Ok(sketch_block_ref)
4326 }
4327
4328 async fn add_lines_equal_length(
4329 &mut self,
4330 sketch: ObjectId,
4331 lines_equal_length: LinesEqualLength,
4332 new_ast: &mut ast::Node<ast::Program>,
4333 ) -> Result<AstNodeRef, KclError> {
4334 if lines_equal_length.lines.len() < 2 {
4335 return Err(KclError::refactor(format!(
4336 "Lines equal length constraint must have at least 2 lines, got {}",
4337 lines_equal_length.lines.len()
4338 )));
4339 };
4340
4341 let sketch_id = sketch;
4342
4343 let line_asts = lines_equal_length
4345 .lines
4346 .iter()
4347 .map(|line_id| {
4348 let line_object = self
4349 .scene_graph
4350 .objects
4351 .get(line_id.0)
4352 .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
4353 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4354 let kind = line_object.kind.human_friendly_kind_with_article();
4355 return Err(KclError::refactor(format!(
4356 "This constraint only works on Segments, but you selected {kind}"
4357 )));
4358 };
4359 let Segment::Line(_) = line_segment else {
4360 let kind = line_segment.human_friendly_kind_with_article();
4361 return Err(KclError::refactor(format!(
4362 "Only lines can be made equal length, but you selected {kind}"
4363 )));
4364 };
4365
4366 self.line_id_to_ast_reference(*line_id, new_ast)
4367 })
4368 .collect::<Result<Vec<_>, _>>()?;
4369
4370 let equal_length_ast = create_equal_length_ast(line_asts);
4372
4373 let (sketch_block_ref, _) = self.mutate_ast(
4375 new_ast,
4376 sketch_id,
4377 AstMutateCommand::AddSketchBlockExprStmt { expr: equal_length_ast },
4378 )?;
4379 Ok(sketch_block_ref)
4380 }
4381
4382 fn equal_radius_segment_id_to_ast_reference(
4383 &mut self,
4384 segment_id: ObjectId,
4385 new_ast: &mut ast::Node<ast::Program>,
4386 ) -> Result<ast::Expr, KclError> {
4387 let segment_object = self
4388 .scene_graph
4389 .objects
4390 .get(segment_id.0)
4391 .ok_or_else(|| KclError::refactor(format!("Segment not found: {segment_id:?}")))?;
4392 let ObjectKind::Segment { segment } = &segment_object.kind else {
4393 return Err(KclError::refactor(format!(
4394 "Object is not a segment, it was {}",
4395 segment_object.kind.human_friendly_kind_with_article()
4396 )));
4397 };
4398
4399 let ref_type = match segment {
4400 Segment::Arc(_) => ARC_VARIABLE,
4401 Segment::Circle(_) => CIRCLE_VARIABLE,
4402 _ => {
4403 return Err(KclError::refactor(format!(
4404 "equalRadius supports only arc/circle segments, got {}",
4405 segment.human_friendly_kind_with_article()
4406 )));
4407 }
4408 };
4409
4410 get_or_insert_ast_reference(new_ast, &segment_object.source, ref_type, None)
4411 }
4412
4413 fn symmetric_input_id_to_ast_reference(
4414 &mut self,
4415 segment_id: ObjectId,
4416 new_ast: &mut ast::Node<ast::Program>,
4417 ) -> Result<ast::Expr, KclError> {
4418 let segment_object = self
4419 .scene_graph
4420 .objects
4421 .get(segment_id.0)
4422 .ok_or_else(|| KclError::refactor(format!("Segment not found: {segment_id:?}")))?;
4423 let ObjectKind::Segment { segment } = &segment_object.kind else {
4424 return Err(KclError::refactor(format!(
4425 "Object is not a segment, it was {}",
4426 segment_object.kind.human_friendly_kind_with_article()
4427 )));
4428 };
4429
4430 match segment {
4431 Segment::Point(_) => self.point_id_to_ast_reference(segment_id, new_ast),
4432 Segment::Line(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, LINE_VARIABLE, None),
4433 Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, ARC_VARIABLE, None),
4434 Segment::Circle(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, CIRCLE_VARIABLE, None),
4435 Segment::ControlPointSpline(_) => Err(KclError::refactor(
4436 "Symmetric does not yet support control point splines".to_owned(),
4437 )),
4438 }
4439 }
4440
4441 fn symmetric_axis_id_to_ast_reference(
4442 &mut self,
4443 segment_id: ObjectId,
4444 new_ast: &mut ast::Node<ast::Program>,
4445 ) -> Result<ast::Expr, KclError> {
4446 let segment_object = self
4447 .scene_graph
4448 .objects
4449 .get(segment_id.0)
4450 .ok_or_else(|| KclError::refactor(format!("Axis segment not found: {segment_id:?}")))?;
4451 let ObjectKind::Segment { segment } = &segment_object.kind else {
4452 return Err(KclError::refactor(format!(
4453 "Object is not a segment, it was {}",
4454 segment_object.kind.human_friendly_kind_with_article()
4455 )));
4456 };
4457 match segment {
4458 Segment::Line(_) => self.line_id_to_ast_reference(segment_id, new_ast),
4459 _ => Err(KclError::refactor(format!(
4460 "Symmetric axis must be a line, got {}",
4461 segment.human_friendly_kind_with_article()
4462 ))),
4463 }
4464 }
4465
4466 async fn add_parallel(
4467 &mut self,
4468 sketch: ObjectId,
4469 parallel: Parallel,
4470 new_ast: &mut ast::Node<ast::Program>,
4471 ) -> Result<AstNodeRef, KclError> {
4472 if parallel.lines.len() < 2 {
4473 return Err(KclError::refactor(format!(
4474 "Parallel constraint must have at least 2 lines, got {}",
4475 parallel.lines.len()
4476 )));
4477 };
4478
4479 let sketch_id = sketch;
4480
4481 let line_asts = parallel
4482 .lines
4483 .iter()
4484 .map(|line_id| {
4485 let line_object = self
4486 .scene_graph
4487 .objects
4488 .get(line_id.0)
4489 .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
4490 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4491 let kind = line_object.kind.human_friendly_kind_with_article();
4492 return Err(KclError::refactor(format!(
4493 "This constraint only works on Segments, but you selected {kind}"
4494 )));
4495 };
4496 let Segment::Line(_) = line_segment else {
4497 let kind = line_segment.human_friendly_kind_with_article();
4498 return Err(KclError::refactor(format!(
4499 "Only lines can be made parallel, but you selected {kind}"
4500 )));
4501 };
4502
4503 self.line_id_to_ast_reference(*line_id, new_ast)
4504 })
4505 .collect::<Result<Vec<_>, _>>()?;
4506
4507 let call_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
4508 callee: ast::Node::no_src(ast_sketch2_name(LinesAtAngleKind::Parallel.to_function_name())),
4509 unlabeled: Some(ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(
4510 ast::ArrayExpression {
4511 elements: line_asts,
4512 digest: None,
4513 non_code_meta: Default::default(),
4514 },
4515 )))),
4516 arguments: Default::default(),
4517 digest: None,
4518 non_code_meta: Default::default(),
4519 })));
4520
4521 let (sketch_block_ref, _) = self.mutate_ast(
4522 new_ast,
4523 sketch_id,
4524 AstMutateCommand::AddSketchBlockExprStmt { expr: call_ast },
4525 )?;
4526 Ok(sketch_block_ref)
4527 }
4528
4529 async fn add_perpendicular(
4530 &mut self,
4531 sketch: ObjectId,
4532 perpendicular: Perpendicular,
4533 new_ast: &mut ast::Node<ast::Program>,
4534 ) -> Result<AstNodeRef, KclError> {
4535 self.add_lines_at_angle_constraint(sketch, LinesAtAngleKind::Perpendicular, perpendicular.lines, new_ast)
4536 .await
4537 }
4538
4539 async fn add_lines_at_angle_constraint(
4540 &mut self,
4541 sketch: ObjectId,
4542 angle_kind: LinesAtAngleKind,
4543 lines: Vec<ObjectId>,
4544 new_ast: &mut ast::Node<ast::Program>,
4545 ) -> Result<AstNodeRef, KclError> {
4546 let &[line0_id, line1_id] = lines.as_slice() else {
4547 return Err(KclError::refactor(format!(
4548 "{} constraint must have exactly 2 lines, got {}",
4549 angle_kind.to_function_name(),
4550 lines.len()
4551 )));
4552 };
4553
4554 let sketch_id = sketch;
4555
4556 let line0_object = self
4558 .scene_graph
4559 .objects
4560 .get(line0_id.0)
4561 .ok_or_else(|| KclError::refactor(format!("Line not found: {line0_id:?}")))?;
4562 let ObjectKind::Segment { segment: line0_segment } = &line0_object.kind else {
4563 let kind = line0_object.kind.human_friendly_kind_with_article();
4564 return Err(KclError::refactor(format!(
4565 "This constraint only works on Segments, but you selected {kind}"
4566 )));
4567 };
4568 let Segment::Line(_) = line0_segment else {
4569 return Err(KclError::refactor(format!(
4570 "Only lines can be made {}, but you selected {}",
4571 angle_kind.to_function_name(),
4572 line0_segment.human_friendly_kind_with_article(),
4573 )));
4574 };
4575 let line0_ast = self.line_id_to_ast_reference(line0_id, new_ast)?;
4576
4577 let line1_object = self
4578 .scene_graph
4579 .objects
4580 .get(line1_id.0)
4581 .ok_or_else(|| KclError::refactor(format!("Line not found: {line1_id:?}")))?;
4582 let ObjectKind::Segment { segment: line1_segment } = &line1_object.kind else {
4583 let kind = line1_object.kind.human_friendly_kind_with_article();
4584 return Err(KclError::refactor(format!(
4585 "This constraint only works on Segments, but you selected {kind}"
4586 )));
4587 };
4588 let Segment::Line(_) = line1_segment else {
4589 return Err(KclError::refactor(format!(
4590 "Only lines can be made {}, but you selected {}",
4591 angle_kind.to_function_name(),
4592 line1_segment.human_friendly_kind_with_article(),
4593 )));
4594 };
4595 let line1_ast = self.line_id_to_ast_reference(line1_id, new_ast)?;
4596
4597 let call_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
4599 callee: ast::Node::no_src(ast_sketch2_name(angle_kind.to_function_name())),
4600 unlabeled: Some(ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(
4601 ast::ArrayExpression {
4602 elements: vec![line0_ast, line1_ast],
4603 digest: None,
4604 non_code_meta: Default::default(),
4605 },
4606 )))),
4607 arguments: Default::default(),
4608 digest: None,
4609 non_code_meta: Default::default(),
4610 })));
4611
4612 let (sketch_block_ref, _) = self.mutate_ast(
4614 new_ast,
4615 sketch_id,
4616 AstMutateCommand::AddSketchBlockExprStmt { expr: call_ast },
4617 )?;
4618 Ok(sketch_block_ref)
4619 }
4620
4621 async fn add_vertical(
4622 &mut self,
4623 sketch: ObjectId,
4624 vertical: Vertical,
4625 new_ast: &mut ast::Node<ast::Program>,
4626 ) -> Result<AstNodeRef, KclError> {
4627 let sketch_id = sketch;
4628
4629 let first_arg_ast = match vertical {
4630 Vertical::Line { line } => {
4631 let line_object = self
4633 .scene_graph
4634 .objects
4635 .get(line.0)
4636 .ok_or_else(|| KclError::refactor(format!("Line not found: {line:?}")))?;
4637 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4638 let kind = line_object.kind.human_friendly_kind_with_article();
4639 return Err(KclError::refactor(format!(
4640 "This constraint only works on Segments, but you selected {kind}"
4641 )));
4642 };
4643 let Segment::Line(_) = line_segment else {
4644 return Err(KclError::refactor(format!(
4645 "Only lines can be made vertical, but you selected {}",
4646 line_segment.human_friendly_kind_with_article()
4647 )));
4648 };
4649 self.line_id_to_ast_reference(line, new_ast)?
4650 }
4651 Vertical::Points { points } => {
4652 let point_asts = points
4653 .iter()
4654 .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
4655 .collect::<Result<Vec<_>, _>>()?;
4656 ast::ArrayExpression::new(point_asts).into()
4657 }
4658 };
4659 let vertical_ast = create_vertical_ast(first_arg_ast);
4661
4662 let (sketch_block_ref, _) = self.mutate_ast(
4664 new_ast,
4665 sketch_id,
4666 AstMutateCommand::AddSketchBlockExprStmt { expr: vertical_ast },
4667 )?;
4668 Ok(sketch_block_ref)
4669 }
4670
4671 async fn execute_after_add_constraint(
4672 &mut self,
4673 ctx: &ExecutorContext,
4674 sketch_id: ObjectId,
4675 sketch_block_ref: AstNodeRef,
4676 new_ast: &mut ast::Node<ast::Program>,
4677 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
4678 let new_source = source_from_ast(new_ast);
4680 let new_program = parse_frontend_mutation_source(
4682 &new_source,
4683 "Error parsing KCL source after adding constraint",
4684 "No AST produced after adding constraint",
4685 )?;
4686 let constraint_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
4687 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
4688 "Source range of new constraint not found in sketch block: {sketch_block_ref:?}; {err:?}"
4689 )))
4690 })?;
4691
4692 let mut truncated_program = new_program.clone();
4695 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
4696 .map_err(KclErrorWithOutputs::no_outputs)?;
4697
4698 let outcome = ctx
4700 .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch_id))
4701 .await?;
4702
4703 let new_object_ids = {
4704 let constraint_id = outcome
4706 .source_range_to_object
4707 .get(&constraint_node_ref.range)
4708 .copied()
4709 .ok_or_else(|| {
4710 KclErrorWithOutputs::from_error_outcome(
4711 KclError::refactor(format!("Source range of constraint not found: {constraint_node_ref:?}")),
4712 outcome.clone(),
4713 )
4714 })?;
4715 vec![constraint_id]
4716 };
4717
4718 self.program = new_program;
4721
4722 let outcome = self.update_state_after_exec(outcome, true);
4724
4725 let src_delta = self.commit_var_solutions_to_program(&outcome, "adding constraint")?;
4726 let scene_graph_delta = SceneGraphDelta {
4727 new_graph: self.scene_graph_for_ui(),
4728 invalidates_ids: false,
4729 new_objects: new_object_ids,
4730 exec_outcome: outcome,
4731 };
4732 Ok((src_delta, scene_graph_delta))
4733 }
4734
4735 fn commit_var_solutions_to_program(&mut self, outcome: &ExecOutcome, operation: &str) -> ExecResult<SourceDelta> {
4736 let commit_failure = || {
4737 KclErrorWithOutputs::from_error_outcome(
4738 KclError::refactor(format!("Could not update KCL after {operation}.")),
4739 outcome.clone(),
4740 )
4741 };
4742
4743 let default_length_unit = self.default_length_unit();
4744 let mut settled_ast = self.program.ast.clone();
4745 let mut committed_solver_value = false;
4746 for (var_range, node_path, value) in &outcome.var_solutions {
4747 let Some(lookup) = numeric_literal_at_node_path(&settled_ast, node_path.as_ref(), *var_range) else {
4748 return Err(commit_failure());
4749 };
4750 let new_value = match &lookup {
4751 Some(current_literal) => {
4752 if !var_solution_needs_commit(current_literal, *value, default_length_unit) {
4753 continue;
4754 }
4755 preserve_var_solution_literal_style(current_literal, *value, default_length_unit)
4756 }
4757 None => {
4758 Number {
4762 value: number_value_in_default_length_units(*value, default_length_unit),
4763 units: default_length_unit.into(),
4764 }
4765 }
4766 };
4767 committed_solver_value = true;
4768 let source_ref = SourceRef::Simple {
4769 range: *var_range,
4770 node_path: node_path.clone(),
4771 };
4772 mutate_ast_node_by_source_ref(
4773 &mut settled_ast,
4774 &source_ref,
4775 AstMutateCommand::EditVarInitialValue { value: new_value },
4776 )
4777 .map_err(|_| commit_failure())?;
4778 }
4779
4780 if !committed_solver_value {
4781 return Ok(SourceDelta {
4782 text: self.program.original_file_contents.clone(),
4783 });
4784 }
4785
4786 let settled_source = source_from_ast(&settled_ast);
4787 let (settled_program, errors) = Program::parse(&settled_source).map_err(|_| commit_failure())?;
4788 if !errors.is_empty() {
4789 return Err(commit_failure());
4790 }
4791 let Some(settled_program) = settled_program else {
4792 return Err(commit_failure());
4793 };
4794
4795 self.program = settled_program;
4796
4797 Ok(SourceDelta { text: settled_source })
4798 }
4799
4800 fn segment_will_be_deleted(&self, segment_id: ObjectId, segment_ids_set: &AhashIndexSet<ObjectId>) -> bool {
4802 if segment_ids_set.contains(&segment_id) {
4803 return true;
4804 }
4805
4806 let Some(segment_object) = self.scene_graph.objects.get(segment_id.0) else {
4807 return false;
4808 };
4809 let ObjectKind::Segment { segment } = &segment_object.kind else {
4810 return false;
4811 };
4812 let Segment::Point(point) = segment else {
4813 return false;
4814 };
4815
4816 point.owner.is_some_and(|owner_id| segment_ids_set.contains(&owner_id))
4817 }
4818
4819 fn remaining_constraint_segments(
4820 &self,
4821 segments: &[ConstraintSegment],
4822 segment_ids_set: &AhashIndexSet<ObjectId>,
4823 ) -> Vec<ConstraintSegment> {
4824 segments
4825 .iter()
4826 .copied()
4827 .filter(|segment| match segment {
4828 ConstraintSegment::Origin(_) => true,
4829 ConstraintSegment::Segment(segment_id) => !self.segment_will_be_deleted(*segment_id, segment_ids_set),
4830 })
4831 .collect()
4832 }
4833
4834 fn find_referenced_constraints(
4835 &self,
4836 sketch_id: ObjectId,
4837 segment_ids_set: &AhashIndexSet<ObjectId>,
4838 ) -> Result<AhashIndexSet<ObjectId>, KclError> {
4839 let sketch_object = self
4841 .scene_graph
4842 .objects
4843 .get(sketch_id.0)
4844 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch_id:?}")))?;
4845 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
4846 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
4847 };
4848 let segment_or_owner_matches = |segment_id: ObjectId| {
4849 if segment_ids_set.contains(&segment_id) {
4850 return true;
4851 }
4852 let segment_object = self.scene_graph.objects.get(segment_id.0);
4853 if let Some(obj) = segment_object
4854 && let ObjectKind::Segment { segment } = &obj.kind
4855 {
4856 match segment {
4857 Segment::Point(point) => point.owner.is_some_and(|owner_id| segment_ids_set.contains(&owner_id)),
4858 Segment::Line(line) => line.owner.is_some_and(|owner_id| segment_ids_set.contains(&owner_id)),
4859 _ => false,
4860 }
4861 } else {
4862 false
4863 }
4864 };
4865 let mut constraint_ids_set = AhashIndexSet::default();
4866 for constraint_id in &sketch.constraints {
4867 let constraint_object = self
4868 .scene_graph
4869 .objects
4870 .get(constraint_id.0)
4871 .ok_or_else(|| KclError::refactor(format!("Constraint not found: {constraint_id:?}")))?;
4872 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
4873 return Err(KclError::refactor(format!(
4874 "Object is not a constraint, it is {}",
4875 constraint_object.kind.human_friendly_kind_with_article()
4876 )));
4877 };
4878 let depends_on_segment = match constraint {
4879 Constraint::Coincident(c) => c.segment_ids().any(segment_or_owner_matches),
4880 Constraint::Distance(d) => d.segment_ids().any(segment_or_owner_matches),
4881 Constraint::Fixed(fixed) => fixed
4882 .points
4883 .iter()
4884 .any(|fixed_point| self.segment_will_be_deleted(fixed_point.point, segment_ids_set)),
4885 Constraint::Radius(r) => segment_or_owner_matches(r.arc),
4886 Constraint::Diameter(d) => segment_or_owner_matches(d.arc),
4887 Constraint::EqualRadius(equal_radius) => {
4888 equal_radius.input.iter().copied().any(segment_or_owner_matches)
4889 }
4890 Constraint::HorizontalDistance(d) => d.segment_ids().any(segment_or_owner_matches),
4891 Constraint::VerticalDistance(d) => d.segment_ids().any(segment_or_owner_matches),
4892 Constraint::Horizontal(h) => match h {
4893 Horizontal::Line { line } => segment_or_owner_matches(*line),
4894 Horizontal::Points { points } => points.iter().any(|point| match point {
4895 ConstraintSegment::Segment(point) => segment_or_owner_matches(*point),
4896 ConstraintSegment::Origin(_) => false,
4897 }),
4898 },
4899 Constraint::Vertical(v) => match v {
4900 Vertical::Line { line } => segment_or_owner_matches(*line),
4901 Vertical::Points { points } => points.iter().any(|point| match point {
4902 ConstraintSegment::Segment(point) => segment_or_owner_matches(*point),
4903 ConstraintSegment::Origin(_) => false,
4904 }),
4905 },
4906 Constraint::LinesEqualLength(lines_equal_length) => {
4907 lines_equal_length.lines.iter().copied().any(segment_or_owner_matches)
4908 }
4909 Constraint::Midpoint(midpoint) => {
4910 segment_or_owner_matches(midpoint.segment)
4911 || matches!(
4912 midpoint.point,
4913 ConstraintSegment::Segment(point) if segment_or_owner_matches(point)
4914 )
4915 }
4916 Constraint::Parallel(parallel) => parallel.lines.iter().copied().any(segment_or_owner_matches),
4917 Constraint::Perpendicular(perpendicular) => {
4918 perpendicular.lines.iter().copied().any(segment_or_owner_matches)
4919 }
4920 Constraint::Angle(angle) => angle.lines.iter().copied().any(segment_or_owner_matches),
4921 Constraint::Symmetric(symmetric) => {
4922 segment_or_owner_matches(symmetric.axis)
4923 || symmetric.input.iter().copied().any(segment_or_owner_matches)
4924 }
4925 Constraint::Tangent(tangent) => tangent.input.iter().copied().any(segment_or_owner_matches),
4926 };
4927 if depends_on_segment {
4928 constraint_ids_set.insert(*constraint_id);
4929 }
4930 }
4931 Ok(constraint_ids_set)
4932 }
4933
4934 fn update_state_after_exec(&mut self, outcome: ExecOutcome, freedom_analysis_ran: bool) -> ExecOutcome {
4935 let mut outcome = outcome;
4936 self.solid_references = solid_references_from_variables(&self.program.ast, &outcome.variables);
4937 let mut new_objects = std::mem::take(&mut outcome.scene_objects);
4938
4939 if freedom_analysis_ran {
4940 self.point_freedom_cache.clear();
4943 for new_obj in &new_objects {
4944 if let ObjectKind::Segment {
4945 segment: kcl_api::Segment::Point(point),
4946 } = &new_obj.kind
4947 {
4948 self.point_freedom_cache.insert(new_obj.id, point.freedom);
4949 }
4950 }
4951 add_wall_and_cap_face_objects(&mut new_objects, &outcome.artifact_graph);
4952 self.scene_graph.objects = new_objects;
4954 } else {
4955 for old_obj in &self.scene_graph.objects {
4958 if let ObjectKind::Segment {
4959 segment: kcl_api::Segment::Point(point),
4960 } = &old_obj.kind
4961 {
4962 self.point_freedom_cache.insert(old_obj.id, point.freedom);
4963 }
4964 }
4965
4966 let mut updated_objects = Vec::with_capacity(new_objects.len());
4968 for new_obj in new_objects {
4969 let mut obj = new_obj;
4970 if let ObjectKind::Segment {
4971 segment: kcl_api::Segment::Point(point),
4972 } = &mut obj.kind
4973 {
4974 let new_freedom = point.freedom;
4975 match new_freedom {
4981 Freedom::Free => {
4982 match self.point_freedom_cache.get(&obj.id).copied() {
4983 Some(Freedom::Conflict) => {
4984 }
4987 Some(Freedom::Fixed) => {
4988 point.freedom = Freedom::Fixed;
4990 }
4991 Some(Freedom::Free) => {
4992 }
4994 None => {
4995 }
4997 }
4998 }
4999 Freedom::Fixed => {
5000 }
5002 Freedom::Conflict => {
5003 }
5005 }
5006 self.point_freedom_cache.insert(obj.id, point.freedom);
5008 }
5009 updated_objects.push(obj);
5010 }
5011
5012 add_wall_and_cap_face_objects(&mut updated_objects, &outcome.artifact_graph);
5013 self.scene_graph.objects = updated_objects;
5014 }
5015 outcome
5016 }
5017
5018 fn mutate_ast(
5019 &mut self,
5020 ast: &mut ast::Node<ast::Program>,
5021 object_id: ObjectId,
5022 command: AstMutateCommand,
5023 ) -> Result<(AstNodeRef, AstMutateCommandReturn), KclError> {
5024 let sketch_object = self
5025 .scene_graph
5026 .objects
5027 .get(object_id.0)
5028 .ok_or_else(|| KclError::refactor(format!("Object not found: {object_id:?}")))?;
5029 mutate_ast_node_by_source_ref(ast, &sketch_object.source, command)
5030 }
5031
5032 fn mutate_constraint_label_position(
5033 &mut self,
5034 ast: &mut ast::Node<ast::Program>,
5035 constraint_id: ObjectId,
5036 label_position: Point2d<Number>,
5037 ) -> Result<(), KclError> {
5038 let object = self
5039 .scene_graph
5040 .objects
5041 .get(constraint_id.0)
5042 .ok_or_else(|| KclError::refactor(format!("Object not found: {constraint_id:?}")))?;
5043 if !matches!(
5044 &object.kind,
5045 ObjectKind::Constraint {
5046 constraint: Constraint::Distance(_)
5047 | Constraint::HorizontalDistance(_)
5048 | Constraint::VerticalDistance(_)
5049 | Constraint::Radius(_)
5050 | Constraint::Diameter(_)
5051 | Constraint::Angle(_),
5052 }
5053 ) {
5054 return Err(KclError::refactor(format!(
5055 "Object does not support labelPosition: {constraint_id:?}"
5056 )));
5057 }
5058
5059 let label_position = to_ast_point2d_number(&label_position)
5060 .map_err(|err| KclError::refactor(format!("Could not convert label position to AST: {err}")))?;
5061 self.mutate_ast(
5062 ast,
5063 constraint_id,
5064 AstMutateCommand::EditDistanceConstraintLabelPosition { label_position },
5065 )?;
5066 Ok(())
5067 }
5068}
5069
5070fn sketch_block_ref_from_id(scene_graph: &SceneGraph, sketch_id: ObjectId) -> Result<AstNodeRef, KclError> {
5071 let sketch_object = scene_graph
5073 .objects
5074 .get(sketch_id.0)
5075 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch_id:?}")))?;
5076 let ObjectKind::Sketch(_) = &sketch_object.kind else {
5077 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
5078 };
5079 expect_single_node_ref(sketch_object)
5080}
5081
5082fn expect_single_node_ref(object: &Object) -> Result<AstNodeRef, KclError> {
5083 match &object.source {
5084 SourceRef::Simple { range, node_path } => Ok(AstNodeRef {
5085 range: *range,
5086 node_path: node_path.clone(),
5087 }),
5088 SourceRef::BackTrace { ranges } => {
5089 let [range] = ranges.as_slice() else {
5090 return Err(KclError::refactor(format!(
5091 "Expected single location in SourceRef, got {}; ranges={ranges:#?}",
5092 ranges.len()
5093 )));
5094 };
5095 Ok(AstNodeRef {
5096 range: range.0,
5097 node_path: range.1.clone(),
5098 })
5099 }
5100 }
5101}
5102
5103fn only_sketch_block_from_range(
5106 ast: &mut ast::Node<ast::Program>,
5107 sketch_block_range: SourceRange,
5108 edit_kind: ChangeKind,
5109) -> Result<(), KclError> {
5110 let r1 = sketch_block_range;
5111 let matches_range = |r2: SourceRange| -> bool {
5112 match edit_kind {
5115 ChangeKind::Add => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() <= r2.end(),
5116 ChangeKind::Edit => r1.module_id() == r2.module_id() && r1.start() == r2.start(),
5118 ChangeKind::Delete => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() >= r2.end(),
5119 ChangeKind::None => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() == r2.end(),
5121 }
5122 };
5123 let mut found = false;
5124 for item in ast.body.iter_mut() {
5125 match item {
5126 ast::BodyItem::ImportStatement(_) => {}
5127 ast::BodyItem::ExpressionStatement(node) => {
5128 if matches_range(SourceRange::from(&*node))
5129 && let ast::Expr::SketchBlock(sketch_block) = &mut node.expression
5130 {
5131 sketch_block.is_being_edited = true;
5132 found = true;
5133 break;
5134 }
5135 }
5136 ast::BodyItem::VariableDeclaration(node) => {
5137 if matches_range(SourceRange::from(&node.declaration.init))
5138 && let ast::Expr::SketchBlock(sketch_block) = &mut node.declaration.init
5139 {
5140 sketch_block.is_being_edited = true;
5141 found = true;
5142 break;
5143 }
5144 }
5145 ast::BodyItem::TypeDeclaration(_) => {}
5146 ast::BodyItem::ReturnStatement(node) => {
5147 if matches_range(SourceRange::from(&node.argument))
5148 && let ast::Expr::SketchBlock(sketch_block) = &mut node.argument
5149 {
5150 sketch_block.is_being_edited = true;
5151 found = true;
5152 break;
5153 }
5154 }
5155 }
5156 }
5157 if !found {
5158 return Err(KclError::refactor(format!(
5159 "Sketch block source range not found in AST: {sketch_block_range:?}, edit_kind={edit_kind:?}"
5160 )));
5161 }
5162
5163 Ok(())
5164}
5165
5166fn only_sketch_block(
5167 ast: &mut ast::Node<ast::Program>,
5168 sketch_block_ref: &AstNodeRef,
5169 edit_kind: ChangeKind,
5170) -> Result<(), KclError> {
5171 let Some(target_node_path) = &sketch_block_ref.node_path else {
5172 #[cfg(target_arch = "wasm32")]
5173 web_sys::console::warn_1(
5174 &format!(
5175 "only_sketch_block: target sketch block ref doesn't have node path; sketch_block_ref={:#?}, edit_kind={edit_kind:#?}",
5176 sketch_block_ref
5177 )
5178 .into(),
5179 );
5180 return only_sketch_block_from_range(ast, sketch_block_ref.range, edit_kind);
5181 };
5182 struct MarkSketchBlockBeingEdited<'a> {
5183 target_node_path: &'a ast::NodePath,
5184 }
5185
5186 impl Visitor for MarkSketchBlockBeingEdited<'_> {
5187 type Break = ();
5188 type Continue = ();
5189
5190 fn visit(&mut self, node: NodeMut<'_>) -> TraversalReturn<Self::Break, Self::Continue> {
5191 if let NodeMut::SketchBlock(sketch_block) = node
5192 && sketch_block.node_path.as_ref() == Some(self.target_node_path)
5193 {
5194 sketch_block.is_being_edited = true;
5195 return TraversalReturn::new_break(());
5196 }
5197 TraversalReturn::new_continue(())
5198 }
5199
5200 fn finish(&mut self, _node: NodeMut<'_>) {}
5201 }
5202
5203 let mut marker = MarkSketchBlockBeingEdited { target_node_path };
5204 let found = dfs_mut(ast, &mut marker).is_break();
5205 if !found {
5206 return Err(KclError::refactor(format!(
5207 "Sketch block node path not found in AST: {sketch_block_ref:?}, edit_kind={edit_kind:?}"
5208 )));
5209 }
5210
5211 Ok(())
5212}
5213
5214fn sketch_on_ast_expr(
5215 ast: &mut ast::Node<ast::Program>,
5216 scene_graph: &SceneGraph,
5217 solid_references: &HashMap<Uuid, SolidAstReference>,
5218 on: &Plane,
5219) -> Result<ast::Expr, KclError> {
5220 match on {
5221 Plane::Default(name) => Ok(default_plane_ast_expr(*name)),
5222 Plane::Object(object_id) => {
5223 let on_object = scene_graph
5224 .objects
5225 .get(object_id.0)
5226 .ok_or_else(|| KclError::refactor(format!("Sketch plane object not found: {object_id:?}")))?;
5227 if let Some(face_expr) = sketch_face_of_scene_object_ast_expr(ast, on_object)? {
5228 return Ok(face_expr);
5229 }
5230 get_or_insert_ast_reference(ast, &on_object.source, "plane", None)
5231 }
5232 Plane::PrimitiveFace(face) => {
5233 let solid_expr = solid_expr_for_engine_id(solid_references, face.solid_id).ok_or_else(|| {
5234 KclError::refactor(format!(
5235 "Could not resolve a KCL solid for selected primitive face: solid_id={}",
5236 face.solid_id
5237 ))
5238 })?;
5239 let face_id_expr = create_face_id_ast(solid_expr.clone(), face.index);
5240 Ok(create_face_of_ast(solid_expr, face_id_expr))
5241 }
5242 }
5243}
5244
5245fn solid_references_from_variables(
5246 ast: &ast::Node<ast::Program>,
5247 variables: &IndexMap<String, KclValueView>,
5248) -> HashMap<Uuid, SolidAstReference> {
5249 let mut references = HashMap::new();
5250
5251 for item in &ast.body {
5254 let ast::BodyItem::VariableDeclaration(declaration) = item else {
5255 continue;
5256 };
5257 let name = &declaration.declaration.id.name;
5258 let Some(value) = variables.get(name) else {
5259 continue;
5260 };
5261
5262 match value {
5263 KclValueView::Solid { value } => {
5264 references.insert(
5265 value.id,
5266 SolidAstReference {
5267 variable_name: name.clone(),
5268 output_index: None,
5269 },
5270 );
5271 }
5272 KclValueView::Tuple { value } | KclValueView::HomArray { value } => {
5273 for (output_index, entry) in value.iter().enumerate() {
5274 if let KclValueView::Solid { value } = entry {
5275 references.insert(
5276 value.id,
5277 SolidAstReference {
5278 variable_name: name.clone(),
5279 output_index: Some(output_index),
5280 },
5281 );
5282 }
5283 }
5284 }
5285 _ => {}
5286 }
5287 }
5288
5289 references
5290}
5291
5292fn solid_expr_for_engine_id(solid_references: &HashMap<Uuid, SolidAstReference>, solid_id: Uuid) -> Option<ast::Expr> {
5293 let reference = solid_references.get(&solid_id)?;
5294 let solid_expr = ast_name_expr(reference.variable_name.clone());
5295 Some(indexed_solid_expr_for_sweep_output(solid_expr, reference.output_index))
5296}
5297
5298fn sketch_face_of_scene_object_ast_expr(
5299 ast: &mut ast::Node<ast::Program>,
5300 on_object: &kcl_api::Object,
5301) -> Result<Option<ast::Expr>, KclError> {
5302 match &on_object.kind {
5303 ObjectKind::Wall(wall) => {
5304 let solid_ref = get_or_insert_ast_reference(
5305 ast,
5306 &source_ref_from_source_ref_range(&wall.source.solid),
5307 "solid",
5308 None,
5309 )?;
5310 let ast::Expr::Name(solid_name_expr) = solid_ref else {
5311 return Err(KclError::refactor(format!(
5312 "Could not resolve solid reference for selected wall: artifact_id={:?}",
5313 on_object.artifact_id
5314 )));
5315 };
5316 let solid_expr = indexed_solid_expr_for_sweep_output(
5317 ast_name_expr(solid_name_expr.name.name.clone()),
5318 wall.solid_output_index,
5319 );
5320 let sweep_ref = get_or_insert_ast_reference(
5321 ast,
5322 &source_ref_from_source_ref_range(&wall.source.sweep),
5323 "solid",
5324 None,
5325 )?;
5326 let ast::Expr::Name(sweep_name_expr) = sweep_ref else {
5327 return Err(KclError::refactor(format!(
5328 "Could not resolve sweep reference for selected wall: artifact_id={:?}",
5329 on_object.artifact_id
5330 )));
5331 };
5332 let sweep_name = sweep_name_expr.name.name.clone();
5333 let segment_ref = get_or_insert_ast_reference(
5334 ast,
5335 &source_ref_from_source_ref_range(&wall.source.segment),
5336 LINE_VARIABLE,
5337 None,
5338 )?;
5339
5340 let face_expr = if let Some(region_name) = region_name_from_sweep_variable(ast, &sweep_name).or_else(|| {
5341 wall.source
5342 .path
5343 .as_ref()
5344 .and_then(|path_source| region_name_from_path_source(ast, path_source))
5345 }) {
5346 let ast::Expr::Name(segment_name_expr) = segment_ref else {
5347 return Err(KclError::refactor(format!(
5348 "Could not resolve source segment reference for selected region wall: artifact_id={:?}",
5349 on_object.artifact_id
5350 )));
5351 };
5352 create_member_expression(
5353 create_member_expression(ast_name_expr(region_name), "tags"),
5354 &segment_name_expr.name.name,
5355 )
5356 } else {
5357 segment_ref
5358 };
5359
5360 Ok(Some(create_face_of_ast(solid_expr, face_expr)))
5361 }
5362 ObjectKind::Cap(cap) => {
5363 let solid_ref =
5364 get_or_insert_ast_reference(ast, &source_ref_from_source_ref_range(&cap.source.solid), "solid", None)?;
5365 let ast::Expr::Name(solid_name_expr) = solid_ref else {
5366 return Err(KclError::refactor(format!(
5367 "Could not resolve solid reference for selected cap: artifact_id={:?}",
5368 on_object.artifact_id
5369 )));
5370 };
5371 let solid_expr = indexed_solid_expr_for_sweep_output(
5372 ast_name_expr(solid_name_expr.name.name.clone()),
5373 cap.solid_output_index,
5374 );
5375 let face_expr = match cap.kind {
5377 kcl_api::CapKind::Start => ast_name_expr("START".to_owned()),
5378 kcl_api::CapKind::End => ast_name_expr("END".to_owned()),
5379 };
5380
5381 Ok(Some(create_face_of_ast(solid_expr, face_expr)))
5382 }
5383 _ => Ok(None),
5384 }
5385}
5386
5387fn indexed_solid_expr_for_sweep_output(solid_expr: ast::Expr, solid_output_index: Option<usize>) -> ast::Expr {
5388 match solid_output_index {
5389 Some(output_index) => create_index_expression(solid_expr, output_index),
5390 None => solid_expr,
5391 }
5392}
5393
5394fn source_ref_from_source_ref_range(source: &SourceRefRange) -> SourceRef {
5395 SourceRef::Simple {
5396 range: source.range,
5397 node_path: source.node_path.clone(),
5398 }
5399}
5400
5401fn region_name_from_path_source(ast: &ast::Node<ast::Program>, path_source: &SourceRefRange) -> Option<String> {
5402 let source_ref = source_ref_from_source_ref_range(path_source);
5403 let candidate = variable_name_containing_source_ref(ast, &source_ref)?;
5404 let ast::Definition::Variable(region_decl) = ast.get_variable(&candidate)? else {
5405 return None;
5406 };
5407 let ast::Expr::CallExpressionKw(region_call) = ®ion_decl.init else {
5408 return None;
5409 };
5410 if region_call.callee.name.name != "region" {
5411 return None;
5412 }
5413 Some(candidate)
5414}
5415
5416fn downstream_composite_code_ref_for_source(artifact_graph: &ArtifactGraph, source_id: ArtifactId) -> Option<&CodeRef> {
5417 let mut current_id = source_id;
5418 let mut current_composite = None;
5419 let mut visited = HashSet::new();
5420
5421 while visited.insert(current_id) {
5422 let next_composite_id = downstream_composite_id_for_solid_source(artifact_graph, current_id);
5423
5424 let Some(composite_id) = next_composite_id else {
5425 break;
5426 };
5427 let Some(Artifact::CompositeSolid(composite)) = artifact_graph.get(&composite_id) else {
5428 break;
5429 };
5430
5431 current_id = composite.id;
5432 current_composite = Some(composite);
5433
5434 if !composite.consumed {
5435 break;
5436 }
5437 }
5438
5439 current_composite.map(|composite| &composite.code_ref)
5440}
5441
5442fn downstream_composite_id_for_solid_source(
5443 artifact_graph: &ArtifactGraph,
5444 source_id: ArtifactId,
5445) -> Option<ArtifactId> {
5446 if let Some(Artifact::Path(path)) = artifact_graph.get(&source_id)
5448 && let Some(composite_id) = path.composite_solid_id
5449 && let Some(Artifact::CompositeSolid(composite)) = artifact_graph.get(&composite_id)
5450 && composite_contains_path_input(&composite.solid_ids, &composite.tool_ids, path.id, path.solid2d_id)
5451 {
5452 return Some(composite_id);
5453 }
5454
5455 for artifact in artifact_graph.values() {
5457 if let Artifact::Path(path) = artifact
5458 && path.sweep_id == Some(source_id)
5459 && let Some(composite_id) = path.composite_solid_id
5460 && let Some(Artifact::CompositeSolid(composite)) = artifact_graph.get(&composite_id)
5461 && composite_contains_path_input(&composite.solid_ids, &composite.tool_ids, path.id, path.solid2d_id)
5462 {
5463 return Some(composite_id);
5464 }
5465 }
5466
5467 artifact_graph.values().find_map(|artifact| {
5469 let Artifact::CompositeSolid(composite) = artifact else {
5470 return None;
5471 };
5472 composite_contains_input(&composite.solid_ids, &composite.tool_ids, source_id).then_some(composite.id)
5473 })
5474}
5475
5476fn composite_contains_path_input(
5477 solid_ids: &[ArtifactId],
5478 tool_ids: &[ArtifactId],
5479 path_id: ArtifactId,
5480 solid2d_id: Option<ArtifactId>,
5481) -> bool {
5482 composite_contains_input(solid_ids, tool_ids, path_id)
5483 || solid2d_id.is_some_and(|solid2d_id| composite_contains_input(solid_ids, tool_ids, solid2d_id))
5484}
5485
5486fn composite_contains_input(solid_ids: &[ArtifactId], tool_ids: &[ArtifactId], input_id: ArtifactId) -> bool {
5487 solid_ids.contains(&input_id) || tool_ids.contains(&input_id)
5488}
5489
5490fn code_ref_source_ref_range(code_ref: &CodeRef) -> SourceRefRange {
5491 let node_path = (!code_ref.node_path.is_empty()).then(|| code_ref.node_path.clone());
5492 SourceRefRange {
5493 range: code_ref.range,
5494 node_path,
5495 }
5496}
5497
5498fn solid_output_index_for_sweep(
5499 artifact_graph: &ArtifactGraph,
5500 sweep_id: ArtifactId,
5501 sweep_code_ref: &CodeRef,
5502) -> Option<usize> {
5503 if downstream_composite_id_for_solid_source(artifact_graph, sweep_id).is_some() {
5506 return None;
5507 }
5508
5509 let sibling_sweeps = artifact_graph
5510 .values()
5511 .filter_map(|artifact| match artifact {
5512 Artifact::Sweep(sweep)
5513 if sweep.code_ref.range == sweep_code_ref.range
5514 && sweep.code_ref.node_path == sweep_code_ref.node_path =>
5515 {
5516 Some(sweep)
5517 }
5518 _ => None,
5519 })
5520 .collect::<Vec<_>>();
5521
5522 if sibling_sweeps.len() <= 1 {
5523 return None;
5524 }
5525
5526 sibling_sweeps
5527 .iter()
5528 .position(|sibling_sweep| sibling_sweep.id == sweep_id)
5529}
5530
5531fn add_wall_and_cap_face_objects(scene_objects: &mut Vec<kcl_api::Object>, artifact_graph: &ArtifactGraph) {
5532 let mut existing_artifact_ids = scene_objects
5533 .iter()
5534 .map(|object| object.artifact_id)
5535 .collect::<HashSet<_>>();
5536
5537 for artifact in artifact_graph.values() {
5538 match artifact {
5539 Artifact::Wall(wall) => {
5540 if existing_artifact_ids.contains(&wall.id) {
5541 continue;
5542 }
5543
5544 let Some(segment) = artifact_graph.get(&wall.seg_id).and_then(|artifact| match artifact {
5545 Artifact::Segment(segment) => Some(segment),
5546 _ => None,
5547 }) else {
5548 continue;
5549 };
5550 let Some(sweep) = artifact_graph.get(&wall.sweep_id).and_then(|artifact| match artifact {
5551 Artifact::Sweep(sweep) => Some(sweep),
5552 _ => None,
5553 }) else {
5554 continue;
5555 };
5556 let source_segment = segment
5557 .original_seg_id
5558 .and_then(|original_seg_id| artifact_graph.get(&original_seg_id))
5559 .and_then(|artifact| match artifact {
5560 Artifact::Segment(segment) => Some(segment),
5561 _ => None,
5562 })
5563 .unwrap_or(segment);
5564 let solid_code_ref =
5565 downstream_composite_code_ref_for_source(artifact_graph, wall.sweep_id).unwrap_or(&sweep.code_ref);
5566 let path_code_ref = artifact_graph
5567 .get(&segment.path_id)
5568 .or_else(|| sweep.path_id.and_then(|path_id| artifact_graph.get(&path_id)))
5569 .and_then(|artifact| match artifact {
5570 Artifact::Path(path) => Some(&path.code_ref),
5571 _ => None,
5572 });
5573 let source = WallSource {
5574 solid: code_ref_source_ref_range(solid_code_ref),
5575 sweep: code_ref_source_ref_range(&sweep.code_ref),
5576 path: path_code_ref.map(code_ref_source_ref_range),
5577 segment: code_ref_source_ref_range(&source_segment.code_ref),
5578 };
5579 let solid_output_index = (solid_code_ref.range == sweep.code_ref.range
5580 && solid_code_ref.node_path == sweep.code_ref.node_path)
5581 .then(|| solid_output_index_for_sweep(artifact_graph, sweep.id, &sweep.code_ref))
5582 .flatten();
5583 let object_source = source_ref_from_source_ref_range(&source.solid);
5584 let id = ObjectId(scene_objects.len());
5585 scene_objects.push(kcl_api::Object {
5586 id,
5587 kind: ObjectKind::Wall(kcl_api::Wall {
5588 id,
5589 source,
5590 solid_output_index,
5591 }),
5592 label: Default::default(),
5593 comments: Default::default(),
5594 artifact_id: wall.id,
5595 source: object_source,
5596 });
5597 existing_artifact_ids.insert(wall.id);
5598 }
5599 Artifact::Cap(cap) => {
5600 if existing_artifact_ids.contains(&cap.id) {
5601 continue;
5602 }
5603
5604 let Some(sweep) = artifact_graph.get(&cap.sweep_id).and_then(|artifact| match artifact {
5605 Artifact::Sweep(sweep) => Some(sweep),
5606 _ => None,
5607 }) else {
5608 continue;
5609 };
5610 let id = ObjectId(scene_objects.len());
5611 let kind = match cap.sub_type {
5612 CapSubType::Start => kcl_api::CapKind::Start,
5613 CapSubType::End => kcl_api::CapKind::End,
5614 };
5615 let solid_code_ref =
5616 downstream_composite_code_ref_for_source(artifact_graph, cap.sweep_id).unwrap_or(&sweep.code_ref);
5617 let source = CapSource {
5618 solid: code_ref_source_ref_range(solid_code_ref),
5619 sweep: code_ref_source_ref_range(&sweep.code_ref),
5620 };
5621 let solid_output_index = (solid_code_ref.range == sweep.code_ref.range
5622 && solid_code_ref.node_path == sweep.code_ref.node_path)
5623 .then(|| solid_output_index_for_sweep(artifact_graph, sweep.id, &sweep.code_ref))
5624 .flatten();
5625 let object_source = source_ref_from_source_ref_range(&source.solid);
5626 scene_objects.push(kcl_api::Object {
5627 id,
5628 kind: ObjectKind::Cap(kcl_api::Cap {
5629 id,
5630 kind,
5631 source,
5632 solid_output_index,
5633 }),
5634 label: Default::default(),
5635 comments: Default::default(),
5636 artifact_id: cap.id,
5637 source: object_source,
5638 });
5639 existing_artifact_ids.insert(cap.id);
5640 }
5641 _ => {}
5642 }
5643 }
5644}
5645
5646fn default_plane_ast_expr(name: crate::engine::PlaneName) -> ast::Expr {
5647 use crate::engine::PlaneName;
5648
5649 match name {
5650 PlaneName::Xy => ast_name_expr("XY".to_owned()),
5651 PlaneName::Xz => ast_name_expr("XZ".to_owned()),
5652 PlaneName::Yz => ast_name_expr("YZ".to_owned()),
5653 PlaneName::NegXy => negated_plane_ast_expr("XY"),
5654 PlaneName::NegXz => negated_plane_ast_expr("XZ"),
5655 PlaneName::NegYz => negated_plane_ast_expr("YZ"),
5656 }
5657}
5658
5659fn negated_plane_ast_expr(name: &str) -> ast::Expr {
5660 ast::Expr::UnaryExpression(BoxNode::new(ast::UnaryExpression::new(
5661 ast::UnaryOperator::Neg,
5662 ast::BinaryPart::Name(BoxNode::new(ast_name(name.to_owned()))),
5663 )))
5664}
5665
5666fn create_face_of_ast(solid_expr: ast::Expr, face_expr: ast::Expr) -> ast::Expr {
5667 ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
5668 callee: ast::Node::no_src(ast_sketch2_name("faceOf")),
5669 unlabeled: Some(solid_expr),
5670 arguments: vec![ast::LabeledArg {
5671 label: Some(ast::Identifier::new("face")),
5672 arg: face_expr,
5673 }],
5674 digest: None,
5675 non_code_meta: Default::default(),
5676 })))
5677}
5678
5679fn create_face_id_ast(solid_expr: ast::Expr, index: usize) -> ast::Expr {
5680 ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
5681 callee: ast::Node::no_src(ast_sketch2_name("faceId")),
5682 unlabeled: Some(solid_expr),
5683 arguments: vec![ast::LabeledArg {
5684 label: Some(ast::Identifier::new("index")),
5685 arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal::from(
5686 ast::NumericLiteral {
5687 value: index as f64,
5688 suffix: NumericSuffix::None,
5689 raw: index.to_string(),
5690 digest: None,
5691 },
5692 )))),
5693 }],
5694 digest: None,
5695 non_code_meta: Default::default(),
5696 })))
5697}
5698
5699fn region_name_from_sweep_variable(ast: &ast::Node<ast::Program>, sweep_variable_name: &str) -> Option<String> {
5700 let ast::Definition::Variable(sweep_decl) = ast.get_variable(sweep_variable_name)? else {
5701 return None;
5702 };
5703 let ast::Expr::CallExpressionKw(sweep_call) = &sweep_decl.init else {
5704 return None;
5705 };
5706 if !matches!(
5707 sweep_call.callee.name.name.as_str(),
5708 "extrude" | "revolve" | "sweep" | "loft"
5709 ) {
5710 return None;
5711 }
5712 let ast::Expr::Name(region_name_expr) = sweep_call.unlabeled.as_ref()? else {
5713 return None;
5714 };
5715 let candidate = region_name_expr.name.name.clone();
5716 let ast::Definition::Variable(region_decl) = ast.get_variable(&candidate)? else {
5717 return None;
5718 };
5719 let ast::Expr::CallExpressionKw(region_call) = ®ion_decl.init else {
5720 return None;
5721 };
5722 if region_call.callee.name.name != "region" {
5723 return None;
5724 }
5725 Some(candidate)
5726}
5727
5728fn get_or_insert_ast_reference(
5735 ast: &mut ast::Node<ast::Program>,
5736 source_ref: &SourceRef,
5737 prefix: &str,
5738 property: Option<&str>,
5739) -> Result<ast::Expr, KclError> {
5740 let command = AstMutateCommand::AddVariableDeclaration {
5741 prefix: prefix.to_owned(),
5742 };
5743 let ret = match mutate_ast_node_by_source_ref(ast, source_ref, command) {
5744 Ok((_, ret)) => ret,
5745 Err(err) => {
5746 if let Some(var_name) = variable_name_containing_source_ref(ast, source_ref) {
5747 AstMutateCommandReturn::Name(var_name)
5748 } else {
5749 return Err(err);
5750 }
5751 }
5752 };
5753 let AstMutateCommandReturn::Name(var_name) = ret else {
5754 return Err(KclError::refactor(
5755 "Expected variable name returned from AddVariableDeclaration".to_owned(),
5756 ));
5757 };
5758 let var_expr = ast::Expr::Name(BoxNode::new(ast::Name::new(&var_name)));
5759 let Some(property) = property else {
5760 return Ok(var_expr);
5762 };
5763
5764 Ok(create_member_expression(var_expr, property))
5765}
5766
5767fn variable_name_containing_source_ref(ast: &ast::Node<ast::Program>, source_ref: &SourceRef) -> Option<String> {
5768 let source_range = match source_ref {
5769 SourceRef::Simple { range, .. } => *range,
5770 SourceRef::BackTrace { ranges } => {
5771 let [range] = ranges.as_slice() else {
5772 return None;
5773 };
5774 range.0
5775 }
5776 };
5777 ast.body.iter().find_map(|item| {
5778 let ast::BodyItem::VariableDeclaration(var_decl) = item else {
5779 return None;
5780 };
5781 let init_range = SourceRange::from(&var_decl.declaration.init);
5782 let source_is_inside_init = init_range.module_id() == source_range.module_id()
5783 && init_range.start() <= source_range.start()
5784 && source_range.end() <= init_range.end();
5785 if matches!(&var_decl.declaration.init, ast::Expr::SketchBlock(_))
5786 && init_range != source_range
5787 && source_is_inside_init
5788 {
5789 return None;
5790 }
5791 source_is_inside_init.then(|| var_decl.name().to_owned())
5792 })
5793}
5794
5795fn mutate_ast_node_by_source_ref(
5796 ast: &mut ast::Node<ast::Program>,
5797 source_ref: &SourceRef,
5798 command: AstMutateCommand,
5799) -> Result<(AstNodeRef, AstMutateCommandReturn), KclError> {
5800 let (source_range, node_path) = match source_ref {
5801 SourceRef::Simple { range, node_path } => (*range, node_path.clone()),
5802 SourceRef::BackTrace { ranges } => {
5803 let [range] = ranges.as_slice() else {
5804 return Err(KclError::refactor(format!(
5805 "Expected single source ref, got {}; ranges={ranges:#?}",
5806 ranges.len(),
5807 )));
5808 };
5809 (range.0, range.1.clone())
5810 }
5811 };
5812 let mut context = AstMutateContext {
5813 source_range,
5814 node_path,
5815 command,
5816 defined_names_stack: Default::default(),
5817 };
5818 let control = dfs_mut(ast, &mut context);
5819 match control {
5820 ControlFlow::Continue(_) => Err(KclError::refactor(
5821 "Could not find the KCL source for this edit. Try reloading the app, or update from code.".to_owned(),
5822 )),
5823 ControlFlow::Break(break_value) => break_value,
5824 }
5825}
5826
5827#[derive(Debug)]
5828struct AstMutateContext {
5829 source_range: SourceRange,
5830 node_path: Option<ast::NodePath>,
5831 command: AstMutateCommand,
5832 defined_names_stack: Vec<HashSet<String>>,
5833}
5834
5835#[derive(Debug)]
5836#[allow(clippy::large_enum_variant)]
5837enum AstMutateCommand {
5838 AddSketchBlockExprStmt {
5840 expr: ast::Expr,
5841 },
5842 AddSketchBlockVarDecl {
5844 prefix: String,
5845 expr: ast::Expr,
5846 },
5847 AddVariableDeclaration {
5848 prefix: String,
5849 },
5850 EditPoint {
5851 at: ast::Expr,
5852 },
5853 EditLine {
5854 start: ast::Expr,
5855 end: ast::Expr,
5856 construction: Option<bool>,
5857 },
5858 EditArc {
5859 start: ast::Expr,
5860 end: ast::Expr,
5861 center: ast::Expr,
5862 direction: Option<ArcDirection>,
5863 construction: Option<bool>,
5864 },
5865 EditCircle {
5866 start: ast::Expr,
5867 center: ast::Expr,
5868 construction: Option<bool>,
5869 },
5870 EditControlPointSpline {
5871 points: ast::Expr,
5872 construction: Option<bool>,
5873 },
5874 EditConstraintValue {
5875 value: ast::BinaryPart,
5876 },
5877 EditAngleConstraint {
5878 call: ast::BinaryPart,
5879 value: ast::BinaryPart,
5880 },
5881 EditDistanceConstraint {
5882 call: ast::BinaryPart,
5883 value: ast::BinaryPart,
5884 },
5885 EditDistanceConstraintLabelPosition {
5886 label_position: ast::Expr,
5887 },
5888 EditCallUnlabeled {
5889 arg: ast::Expr,
5890 },
5891 EditVarInitialValue {
5892 value: Number,
5893 },
5894 DeleteNode,
5895}
5896
5897impl AstMutateCommand {
5898 fn needs_defined_names_stack(&self) -> bool {
5899 matches!(
5900 self,
5901 AstMutateCommand::AddSketchBlockVarDecl { .. } | AstMutateCommand::AddVariableDeclaration { .. }
5902 )
5903 }
5904}
5905
5906#[derive(Debug)]
5907enum AstMutateCommandReturn {
5908 None,
5909 Name(String),
5910}
5911
5912#[derive(Debug, Clone)]
5913struct AstNodeRef {
5914 range: SourceRange,
5915 node_path: Option<ast::NodePath>,
5916}
5917
5918impl<T> From<&ast::Node<T>> for AstNodeRef {
5919 fn from(value: &ast::Node<T>) -> Self {
5920 AstNodeRef {
5921 range: value.into(),
5922 node_path: value.node_path.clone(),
5923 }
5924 }
5925}
5926
5927impl From<&ast::BodyItem> for AstNodeRef {
5928 fn from(value: &ast::BodyItem) -> Self {
5929 match value {
5930 ast::BodyItem::ImportStatement(node) => AstNodeRef {
5931 range: node.into(),
5932 node_path: node.node_path.clone(),
5933 },
5934 ast::BodyItem::ExpressionStatement(node) => AstNodeRef {
5935 range: node.into(),
5936 node_path: node.node_path.clone(),
5937 },
5938 ast::BodyItem::VariableDeclaration(node) => AstNodeRef {
5939 range: node.into(),
5940 node_path: node.node_path.clone(),
5941 },
5942 ast::BodyItem::TypeDeclaration(node) => AstNodeRef {
5943 range: node.into(),
5944 node_path: node.node_path.clone(),
5945 },
5946 ast::BodyItem::ReturnStatement(node) => AstNodeRef {
5947 range: node.into(),
5948 node_path: node.node_path.clone(),
5949 },
5950 }
5951 }
5952}
5953
5954impl From<&ast::Expr> for AstNodeRef {
5955 fn from(value: &ast::Expr) -> Self {
5956 AstNodeRef {
5957 range: SourceRange::from(value),
5958 node_path: value.node_path().cloned(),
5959 }
5960 }
5961}
5962
5963impl From<&AstMutateContext> for AstNodeRef {
5964 fn from(value: &AstMutateContext) -> Self {
5965 AstNodeRef {
5966 range: value.source_range,
5967 node_path: value.node_path.clone(),
5968 }
5969 }
5970}
5971
5972impl TryFrom<&NodeMut<'_>> for AstNodeRef {
5973 type Error = crate::walk::AstNodeError;
5974
5975 fn try_from(value: &NodeMut<'_>) -> Result<Self, Self::Error> {
5976 Ok(AstNodeRef {
5977 range: SourceRange::try_from(value)?,
5978 node_path: value.try_into()?,
5979 })
5980 }
5981}
5982
5983impl From<AstNodeRef> for SourceRange {
5984 fn from(value: AstNodeRef) -> Self {
5985 value.range
5986 }
5987}
5988
5989impl Visitor for AstMutateContext {
5990 type Break = Result<(AstNodeRef, AstMutateCommandReturn), KclError>;
5991 type Continue = ();
5992
5993 fn visit(&mut self, node: NodeMut<'_>) -> TraversalReturn<Self::Break, Self::Continue> {
5994 filter_and_process(self, node)
5995 }
5996
5997 fn finish(&mut self, node: NodeMut<'_>) {
5998 match &node {
5999 NodeMut::Program(_) | NodeMut::SketchBlock(_) => {
6000 self.defined_names_stack.pop();
6001 }
6002 _ => {}
6003 }
6004 }
6005}
6006
6007fn filter_and_process(
6008 ctx: &mut AstMutateContext,
6009 node: NodeMut,
6010) -> TraversalReturn<Result<(AstNodeRef, AstMutateCommandReturn), KclError>> {
6011 let Ok(node_range) = SourceRange::try_from(&node) else {
6012 return TraversalReturn::new_continue(());
6014 };
6015 if let NodeMut::VariableDeclaration(var_decl) = &node {
6020 let expr_range = SourceRange::from(&var_decl.declaration.init);
6021 let expr_node_path = var_decl.declaration.init.node_path();
6022 if source_ref_matches(ctx, expr_range, expr_node_path) {
6023 if let AstMutateCommand::AddVariableDeclaration { .. } = &ctx.command {
6024 return TraversalReturn::new_break(Ok((
6027 AstNodeRef::from(&**var_decl),
6028 AstMutateCommandReturn::Name(var_decl.name().to_owned()),
6029 )));
6030 }
6031 if let AstMutateCommand::DeleteNode = &ctx.command {
6032 return TraversalReturn {
6035 mutate_body_item: MutateBodyItem::Delete,
6036 control_flow: ControlFlow::Break(Ok((AstNodeRef::from(&*ctx), AstMutateCommandReturn::None))),
6037 };
6038 }
6039 }
6040 }
6041 if let NodeMut::ExpressionStatement(expr_stmt) = &node {
6044 let expr_range = SourceRange::from(&expr_stmt.expression);
6045 let expr_node_path = expr_stmt.expression.node_path();
6046 if source_ref_matches(ctx, expr_range, expr_node_path) {
6047 if let AstMutateCommand::AddVariableDeclaration { .. } = &ctx.command {
6048 let Ok(node_ref) = AstNodeRef::try_from(&node) else {
6051 return TraversalReturn::new_continue(());
6052 };
6053 return process(ctx, node).map_break(|result| result.map(|cmd_return| (node_ref, cmd_return)));
6054 }
6055 if let AstMutateCommand::DeleteNode = &ctx.command {
6056 return TraversalReturn {
6059 mutate_body_item: MutateBodyItem::Delete,
6060 control_flow: ControlFlow::Break(Ok((AstNodeRef::from(&*ctx), AstMutateCommandReturn::None))),
6061 };
6062 }
6063 }
6064 }
6065
6066 if ctx.command.needs_defined_names_stack() {
6067 if let NodeMut::Program(program) = &node {
6068 ctx.defined_names_stack.push(find_defined_names(*program));
6069 } else if let NodeMut::SketchBlock(block) = &node {
6070 ctx.defined_names_stack.push(find_defined_names(&block.body));
6071 }
6072 }
6073
6074 let node_path = <Option<ast::NodePath>>::try_from(&node).ok().flatten();
6076 if !source_ref_matches(ctx, node_range, node_path.as_ref()) {
6077 return TraversalReturn::new_continue(());
6078 }
6079 let Ok(node_ref) = AstNodeRef::try_from(&node) else {
6080 return TraversalReturn::new_continue(());
6081 };
6082 process(ctx, node).map_break(|result| result.map(|cmd_return| (node_ref, cmd_return)))
6083}
6084
6085fn source_ref_matches(ctx: &AstMutateContext, node_range: SourceRange, node_path: Option<&ast::NodePath>) -> bool {
6086 match &ctx.node_path {
6087 Some(target) => Some(target) == node_path,
6088 None => node_range == ctx.source_range,
6089 }
6090}
6091
6092fn is_angle_constraint_call_name(name: &str) -> bool {
6093 matches!(name, ANGLE_FN | ANGLE_DIMENSION_FN)
6094}
6095
6096fn is_distance_constraint_call_name(name: &str) -> bool {
6097 matches!(name, DISTANCE_FN | HORIZONTAL_DISTANCE_FN | VERTICAL_DISTANCE_FN)
6098}
6099
6100fn is_constraint_call_name(name: &str) -> bool {
6101 matches!(
6102 name,
6103 DISTANCE_FN
6104 | HORIZONTAL_DISTANCE_FN
6105 | VERTICAL_DISTANCE_FN
6106 | RADIUS_FN
6107 | DIAMETER_FN
6108 | ANGLE_FN
6109 | ANGLE_DIMENSION_FN
6110 )
6111}
6112
6113fn constraint_supports_label_position(part: &mut ast::BinaryPart) -> Option<&mut BoxNode<CallExpressionKw>> {
6114 if let ast::BinaryPart::CallExpressionKw(call) = part
6115 && is_constraint_call_name(call.callee.name.name.as_str())
6116 {
6117 Some(call)
6118 } else {
6119 None
6120 }
6121}
6122
6123fn process(ctx: &AstMutateContext, node: NodeMut) -> TraversalReturn<Result<AstMutateCommandReturn, KclError>> {
6124 match &ctx.command {
6125 AstMutateCommand::AddSketchBlockExprStmt { expr } => {
6126 if let NodeMut::SketchBlock(sketch_block) = node {
6127 sketch_block
6128 .body
6129 .items
6130 .push(ast::BodyItem::ExpressionStatement(ast::Node {
6131 inner: ast::ExpressionStatement {
6132 expression: expr.clone(),
6133 digest: None,
6134 },
6135 start: Default::default(),
6136 end: Default::default(),
6137 module_id: Default::default(),
6138 node_path: None,
6139 outer_attrs: Default::default(),
6140 pre_comments: Default::default(),
6141 comment_start: Default::default(),
6142 }));
6143 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6144 }
6145 }
6146 AstMutateCommand::AddSketchBlockVarDecl { prefix, expr } => {
6147 if let NodeMut::SketchBlock(sketch_block) = node {
6148 let empty_defined_names = HashSet::new();
6149 let defined_names = ctx.defined_names_stack.last().unwrap_or(&empty_defined_names);
6150 let Ok(name) = next_free_name(prefix, defined_names) else {
6151 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6152 };
6153 sketch_block
6154 .body
6155 .items
6156 .push(ast::BodyItem::VariableDeclaration(BoxNode::new(ast::Node::no_src(
6157 ast::VariableDeclaration::new(
6158 ast::VariableDeclarator::new(&name, expr.clone()),
6159 ast::ItemVisibility::Default,
6160 ast::VariableKind::Const,
6161 ),
6162 ))));
6163 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::Name(name)));
6164 }
6165 }
6166 AstMutateCommand::AddVariableDeclaration { prefix } => {
6167 if let NodeMut::VariableDeclaration(inner) = node {
6168 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::Name(inner.name().to_owned())));
6169 }
6170 if let NodeMut::ExpressionStatement(expr_stmt) = node {
6171 let empty_defined_names = HashSet::new();
6172 let defined_names = ctx.defined_names_stack.last().unwrap_or(&empty_defined_names);
6173 let Ok(name) = next_free_name(prefix, defined_names) else {
6174 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6176 };
6177 let mutate_node =
6178 ast::BodyItem::VariableDeclaration(BoxNode::new(ast::Node::no_src(ast::VariableDeclaration::new(
6179 ast::VariableDeclarator::new(&name, expr_stmt.expression.clone()),
6180 ast::ItemVisibility::Default,
6181 ast::VariableKind::Const,
6182 ))));
6183 return TraversalReturn {
6184 mutate_body_item: MutateBodyItem::Mutate(Box::new(mutate_node)),
6185 control_flow: ControlFlow::Break(Ok(AstMutateCommandReturn::Name(name))),
6186 };
6187 }
6188 }
6189 AstMutateCommand::EditPoint { at } => {
6190 if let NodeMut::CallExpressionKw(call) = node {
6191 if call.callee.name.name != POINT_FN {
6192 return TraversalReturn::new_continue(());
6193 }
6194 for labeled_arg in &mut call.arguments {
6196 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(POINT_AT_PARAM) {
6197 labeled_arg.arg = at.clone();
6198 }
6199 }
6200 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6201 }
6202 }
6203 AstMutateCommand::EditLine {
6204 start,
6205 end,
6206 construction,
6207 } => {
6208 if let NodeMut::CallExpressionKw(call) = node {
6209 if call.callee.name.name != LINE_FN {
6210 return TraversalReturn::new_continue(());
6211 }
6212 for labeled_arg in &mut call.arguments {
6214 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(LINE_START_PARAM) {
6215 labeled_arg.arg = start.clone();
6216 }
6217 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(LINE_END_PARAM) {
6218 labeled_arg.arg = end.clone();
6219 }
6220 }
6221 if let Some(construction_value) = construction {
6223 let construction_exists = call
6224 .arguments
6225 .iter()
6226 .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
6227 if *construction_value {
6228 if construction_exists {
6230 for labeled_arg in &mut call.arguments {
6232 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
6233 labeled_arg.arg =
6234 ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6235 value: ast::LiteralValue::Bool(true),
6236 raw: "true".to_string(),
6237 digest: None,
6238 })));
6239 }
6240 }
6241 } else {
6242 call.arguments.push(ast::LabeledArg {
6244 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
6245 arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6246 value: ast::LiteralValue::Bool(true),
6247 raw: "true".to_string(),
6248 digest: None,
6249 }))),
6250 });
6251 }
6252 } else {
6253 call.arguments
6255 .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
6256 }
6257 }
6258 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6259 }
6260 }
6261 AstMutateCommand::EditArc {
6262 start,
6263 end,
6264 center,
6265 direction,
6266 construction,
6267 } => {
6268 if let NodeMut::CallExpressionKw(call) = node {
6269 if call.callee.name.name != ARC_FN {
6270 return TraversalReturn::new_continue(());
6271 }
6272 for labeled_arg in &mut call.arguments {
6274 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_START_PARAM) {
6275 labeled_arg.arg = start.clone();
6276 }
6277 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_END_PARAM) {
6278 labeled_arg.arg = end.clone();
6279 }
6280 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_CENTER_PARAM) {
6281 labeled_arg.arg = center.clone();
6282 }
6283 }
6284 if let Some(direction_value) = direction {
6286 let direction_exists = call
6287 .arguments
6288 .iter()
6289 .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_DIRECTION_PARAM));
6290 if direction_value.is_clockwise() {
6291 let direction_ast = ast::Expr::Name(BoxNode::new(ast::Name::new(ARC_DIRECTION_CW_NAME)));
6292 if direction_exists {
6293 for labeled_arg in &mut call.arguments {
6295 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_DIRECTION_PARAM) {
6296 labeled_arg.arg = direction_ast.clone();
6297 }
6298 }
6299 } else {
6300 call.arguments.push(ast::LabeledArg {
6302 label: Some(ast::Identifier::new(ARC_DIRECTION_PARAM)),
6303 arg: direction_ast,
6304 });
6305 }
6306 } else {
6307 call.arguments
6310 .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(ARC_DIRECTION_PARAM));
6311 }
6312 }
6313 if let Some(construction_value) = construction {
6315 let construction_exists = call
6316 .arguments
6317 .iter()
6318 .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
6319 if *construction_value {
6320 if construction_exists {
6322 for labeled_arg in &mut call.arguments {
6324 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
6325 labeled_arg.arg =
6326 ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6327 value: ast::LiteralValue::Bool(true),
6328 raw: "true".to_string(),
6329 digest: None,
6330 })));
6331 }
6332 }
6333 } else {
6334 call.arguments.push(ast::LabeledArg {
6336 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
6337 arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6338 value: ast::LiteralValue::Bool(true),
6339 raw: "true".to_string(),
6340 digest: None,
6341 }))),
6342 });
6343 }
6344 } else {
6345 call.arguments
6347 .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
6348 }
6349 }
6350 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6351 }
6352 }
6353 AstMutateCommand::EditCircle {
6354 start,
6355 center,
6356 construction,
6357 } => {
6358 if let NodeMut::CallExpressionKw(call) = node {
6359 if call.callee.name.name != CIRCLE_FN {
6360 return TraversalReturn::new_continue(());
6361 }
6362 for labeled_arg in &mut call.arguments {
6364 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CIRCLE_START_PARAM) {
6365 labeled_arg.arg = start.clone();
6366 }
6367 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CIRCLE_CENTER_PARAM) {
6368 labeled_arg.arg = center.clone();
6369 }
6370 }
6371 if let Some(construction_value) = construction {
6373 let construction_exists = call
6374 .arguments
6375 .iter()
6376 .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
6377 if *construction_value {
6378 if construction_exists {
6379 for labeled_arg in &mut call.arguments {
6381 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
6382 labeled_arg.arg =
6383 ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6384 value: ast::LiteralValue::Bool(true),
6385 raw: "true".to_string(),
6386 digest: None,
6387 })));
6388 }
6389 }
6390 } else {
6391 call.arguments.push(ast::LabeledArg {
6393 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
6394 arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6395 value: ast::LiteralValue::Bool(true),
6396 raw: "true".to_string(),
6397 digest: None,
6398 }))),
6399 });
6400 }
6401 } else {
6402 call.arguments
6404 .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
6405 }
6406 }
6407 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6408 }
6409 }
6410 AstMutateCommand::EditControlPointSpline { points, construction } => {
6411 if let NodeMut::CallExpressionKw(call) = node {
6412 if call.callee.name.name != CONTROL_POINT_SPLINE_FN {
6413 return TraversalReturn::new_continue(());
6414 }
6415 for labeled_arg in &mut call.arguments {
6416 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONTROL_POINT_SPLINE_POINTS_PARAM)
6417 {
6418 labeled_arg.arg = points.clone();
6419 }
6420 }
6421 if let Some(construction_value) = construction {
6423 let construction_exists = call
6424 .arguments
6425 .iter()
6426 .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
6427 if *construction_value {
6428 if construction_exists {
6429 for labeled_arg in &mut call.arguments {
6430 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
6431 labeled_arg.arg =
6432 ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6433 value: ast::LiteralValue::Bool(true),
6434 raw: "true".to_string(),
6435 digest: None,
6436 })));
6437 }
6438 }
6439 } else {
6440 call.arguments.push(ast::LabeledArg {
6441 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
6442 arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6443 value: ast::LiteralValue::Bool(true),
6444 raw: "true".to_string(),
6445 digest: None,
6446 }))),
6447 });
6448 }
6449 } else {
6450 call.arguments
6451 .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
6452 }
6453 }
6454 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6455 }
6456 }
6457 AstMutateCommand::EditConstraintValue { value } => {
6458 if let NodeMut::BinaryExpression(binary_expr) = node {
6459 let left_is_constraint = matches!(
6460 &binary_expr.left,
6461 ast::BinaryPart::CallExpressionKw(call) if is_constraint_call_name(call.callee.name.name.as_str())
6462 );
6463 if left_is_constraint {
6464 binary_expr.right = value.clone();
6465 } else {
6466 binary_expr.left = value.clone();
6467 }
6468
6469 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6470 }
6471 }
6472 AstMutateCommand::EditAngleConstraint { call, value } => {
6473 if let NodeMut::BinaryExpression(binary_expr) = node {
6474 let left_is_angle = matches!(
6475 &binary_expr.left,
6476 ast::BinaryPart::CallExpressionKw(existing_call)
6477 if is_angle_constraint_call_name(existing_call.callee.name.name.as_str())
6478 );
6479 let right_is_angle = matches!(
6480 &binary_expr.right,
6481 ast::BinaryPart::CallExpressionKw(existing_call)
6482 if is_angle_constraint_call_name(existing_call.callee.name.name.as_str())
6483 );
6484
6485 match (left_is_angle, right_is_angle) {
6486 (true, _) => {
6487 binary_expr.left = call.clone();
6488 binary_expr.right = value.clone();
6489 }
6490 (false, true) => {
6491 binary_expr.left = value.clone();
6492 binary_expr.right = call.clone();
6493 }
6494 (false, false) => return TraversalReturn::new_continue(()),
6495 }
6496
6497 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6498 }
6499 }
6500 AstMutateCommand::EditDistanceConstraint { call, value } => {
6501 if let NodeMut::BinaryExpression(binary_expr) = node {
6502 let left_is_distance = matches!(
6503 &binary_expr.left,
6504 ast::BinaryPart::CallExpressionKw(existing_call)
6505 if is_distance_constraint_call_name(existing_call.callee.name.name.as_str())
6506 );
6507 let right_is_distance = matches!(
6508 &binary_expr.right,
6509 ast::BinaryPart::CallExpressionKw(existing_call)
6510 if is_distance_constraint_call_name(existing_call.callee.name.name.as_str())
6511 );
6512
6513 match (left_is_distance, right_is_distance) {
6514 (true, _) => {
6515 binary_expr.left = call.clone();
6516 binary_expr.right = value.clone();
6517 }
6518 (false, true) => {
6519 binary_expr.left = value.clone();
6520 binary_expr.right = call.clone();
6521 }
6522 (false, false) => return TraversalReturn::new_continue(()),
6523 }
6524
6525 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6526 }
6527 }
6528 AstMutateCommand::EditDistanceConstraintLabelPosition { label_position } => {
6529 if let NodeMut::BinaryExpression(binary_expr) = node {
6530 let call = if let Some(call) = constraint_supports_label_position(&mut binary_expr.left) {
6531 call
6532 } else if let Some(call) = constraint_supports_label_position(&mut binary_expr.right) {
6533 call
6534 } else {
6535 return TraversalReturn::new_continue(());
6536 };
6537
6538 if let Some(label_arg) = call
6539 .arguments
6540 .iter_mut()
6541 .find(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(LABEL_POSITION_PARAM))
6542 {
6543 label_arg.arg = label_position.clone();
6544 } else {
6545 call.arguments.push(ast::LabeledArg {
6546 label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
6547 arg: label_position.clone(),
6548 });
6549 }
6550
6551 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6552 }
6553 }
6554 AstMutateCommand::EditCallUnlabeled { arg } => {
6555 if let NodeMut::CallExpressionKw(call) = node {
6556 call.unlabeled = Some(arg.clone());
6557 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6558 }
6559 }
6560 AstMutateCommand::EditVarInitialValue { value } => {
6561 if let NodeMut::SketchVar(sketch_var) = node {
6565 let Ok(literal) = to_source_number(*value) else {
6566 return TraversalReturn::new_break(Err(KclError::refactor(format!(
6567 "Could not convert number to AST literal: {:?}",
6568 *value
6569 ))));
6570 };
6571 sketch_var.initial = Some(BoxNode::new(ast::Node::no_src(literal)));
6572 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6573 }
6574 }
6575 AstMutateCommand::DeleteNode => {
6576 return TraversalReturn {
6577 mutate_body_item: MutateBodyItem::Delete,
6578 control_flow: ControlFlow::Break(Ok(AstMutateCommandReturn::None)),
6579 };
6580 }
6581 }
6582 TraversalReturn::new_continue(())
6583}
6584
6585struct FindSketchBlockSourceRange {
6586 target_before_mutation: SourceRange,
6588 found: Cell<Option<AstNodeRef>>,
6592}
6593
6594impl<'a> crate::walk::Visitor<'a> for &FindSketchBlockSourceRange {
6595 type Error = crate::front::Error;
6596
6597 fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
6598 let Ok(node_range) = SourceRange::try_from(&node) else {
6599 return Ok(true);
6600 };
6601
6602 if let crate::walk::Node::SketchBlock(sketch_block) = node {
6603 if node_range.module_id() == self.target_before_mutation.module_id()
6604 && node_range.start() == self.target_before_mutation.start()
6605 && node_range.end() >= self.target_before_mutation.end()
6607 {
6608 self.found.set(sketch_block.body.items.last().map(|item| match item {
6609 ast::BodyItem::VariableDeclaration(node) => AstNodeRef::from(&node.declaration.init),
6613 _ => AstNodeRef::from(item),
6614 }));
6615 return Ok(false);
6616 } else {
6617 return Ok(true);
6620 }
6621 }
6622
6623 for child in node.children().iter() {
6624 if !child.visit(*self)? {
6625 return Ok(false);
6626 }
6627 }
6628
6629 Ok(true)
6630 }
6631}
6632
6633struct FindSketchBlockByNodePath {
6634 target_node_path: ast::NodePath,
6636 found: Cell<Option<AstNodeRef>>,
6640}
6641
6642impl<'a> crate::walk::Visitor<'a> for &FindSketchBlockByNodePath {
6643 type Error = crate::front::Error;
6644
6645 fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
6646 let Ok(node_path) = <Option<ast::NodePath>>::try_from(&node) else {
6647 return Ok(true);
6648 };
6649
6650 if let crate::walk::Node::SketchBlock(sketch_block) = node {
6651 if let Some(node_path) = node_path
6652 && node_path == self.target_node_path
6653 {
6654 self.found.set(sketch_block.body.items.last().map(|item| match item {
6655 ast::BodyItem::VariableDeclaration(node) => AstNodeRef::from(&node.declaration.init),
6659 _ => AstNodeRef::from(item),
6660 }));
6661
6662 return Ok(false);
6663 } else {
6664 return Ok(true);
6667 }
6668 }
6669
6670 for child in node.children().iter() {
6671 if !child.visit(*self)? {
6672 return Ok(false);
6673 }
6674 }
6675
6676 Ok(true)
6677 }
6678}
6679
6680fn find_sketch_block_added_item(
6688 ast: &ast::Node<ast::Program>,
6689 sketch_block_before_mutation: &AstNodeRef,
6690) -> Result<AstNodeRef, KclError> {
6691 if let Some(node_path) = &sketch_block_before_mutation.node_path {
6692 let find = FindSketchBlockByNodePath {
6693 target_node_path: node_path.clone(),
6694 found: Cell::new(None),
6695 };
6696 let node = crate::walk::Node::from(ast);
6697 node.visit(&find).map_err(|err| KclError::refactor(err.msg))?;
6698 find.found.into_inner().ok_or_else(|| {
6699 KclError::refactor(format!(
6700 "Node ID after mutation not found for Node ID before mutation: {node_path:?}"
6701 ))
6702 })
6703 } else {
6704 let find = FindSketchBlockSourceRange {
6706 target_before_mutation: sketch_block_before_mutation.range,
6707 found: Cell::new(None),
6708 };
6709 let node = crate::walk::Node::from(ast);
6710 node.visit(&find).map_err(|err| KclError::refactor(err.msg))?;
6711 find.found.into_inner().ok_or_else(|| KclError::refactor(
6712 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?"),
6713 ))
6714 }
6715}
6716
6717fn format_kcl_error_message(prefix: &str, error: &KclError) -> String {
6718 let message = error.message().trim();
6719 let message = if message.is_empty() {
6720 "unknown parse error"
6721 } else {
6722 message
6723 };
6724
6725 format!("{prefix}: {message}")
6726}
6727
6728fn parse_frontend_mutation_source(source: &str, parse_error_prefix: &str, no_ast_message: &str) -> ExecResult<Program> {
6729 let (program, errors) = Program::parse(source).map_err(|err| {
6730 KclErrorWithOutputs::no_outputs(KclError::refactor(format_kcl_error_message(parse_error_prefix, &err)))
6731 })?;
6732 if !errors.is_empty() {
6733 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
6734 format_compilation_issues(parse_error_prefix, &errors),
6735 )));
6736 }
6737
6738 program.ok_or_else(|| KclErrorWithOutputs::no_outputs(KclError::refactor(no_ast_message.to_owned())))
6739}
6740
6741fn format_compilation_issues(prefix: &str, issues: &[CompilationIssue]) -> String {
6742 let Some(first_issue) = issues
6743 .iter()
6744 .find(|issue| issue.severity.is_err())
6745 .or_else(|| issues.first())
6746 else {
6747 return prefix.to_owned();
6748 };
6749
6750 let message = first_issue.message.trim();
6751 let message = if message.is_empty() {
6752 "unknown parse error"
6753 } else {
6754 message
6755 };
6756
6757 if issues.len() > 1 {
6758 format!("{prefix}: {message} (+{} more)", issues.len() - 1)
6759 } else {
6760 format!("{prefix}: {message}")
6761 }
6762}
6763
6764fn source_from_ast(ast: &ast::Node<ast::Program>) -> String {
6765 ast.recast_top(&Default::default(), 0)
6767}
6768
6769struct FindNumericLiteral {
6770 target: SourceRange,
6771 found: Cell<Option<ast::NumericLiteral>>,
6772}
6773
6774impl<'a> crate::walk::Visitor<'a> for &FindNumericLiteral {
6775 type Error = crate::front::Error;
6776
6777 fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
6778 let Ok(node_range) = SourceRange::try_from(&node) else {
6779 return Ok(true);
6780 };
6781
6782 if node_range == self.target
6783 && let crate::walk::Node::NumericLiteral(literal) = node
6784 {
6785 self.found.set(Some(literal.inner.clone()));
6786 return Ok(false);
6787 }
6788
6789 for child in node.children().iter() {
6790 if !child.visit(*self)? {
6791 return Ok(false);
6792 }
6793 }
6794
6795 Ok(true)
6796 }
6797}
6798
6799fn numeric_literal_at_source_range(ast: &ast::Node<ast::Program>, target: SourceRange) -> Option<ast::NumericLiteral> {
6800 let find = FindNumericLiteral {
6801 target,
6802 found: Cell::new(None),
6803 };
6804 let node = crate::walk::Node::from(ast);
6805 node.visit(&find).ok()?;
6806 find.found.into_inner()
6807}
6808
6809struct FindSketchVarInitialByNodePath<'a> {
6810 target: &'a ast::NodePath,
6811 sketch_var_found: Cell<bool>,
6812 initial_literal: Cell<Option<ast::NumericLiteral>>,
6813}
6814
6815impl<'a, 'b> crate::walk::Visitor<'b> for &FindSketchVarInitialByNodePath<'a> {
6816 type Error = crate::front::Error;
6817
6818 fn visit_node(&self, node: crate::walk::Node<'b>) -> anyhow::Result<bool, Self::Error> {
6819 if let crate::walk::Node::SketchVar(sketch_var) = node
6820 && sketch_var.node_path.as_ref() == Some(self.target)
6821 {
6822 self.sketch_var_found.set(true);
6823 if let Some(initial) = &sketch_var.initial {
6824 self.initial_literal.set(Some(initial.inner.clone()));
6825 }
6826 return Ok(false);
6827 }
6828
6829 for child in node.children().iter() {
6830 if !child.visit(*self)? {
6831 return Ok(false);
6832 }
6833 }
6834
6835 Ok(true)
6836 }
6837}
6838
6839fn numeric_literal_at_node_path(
6849 ast: &ast::Node<ast::Program>,
6850 node_path: Option<&ast::NodePath>,
6851 source_range: SourceRange,
6852) -> Option<Option<ast::NumericLiteral>> {
6853 let Some(node_path) = node_path else {
6854 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";
6855 #[cfg(target_arch = "wasm32")]
6856 web_sys::console::warn_1(&message.into());
6857 #[cfg(not(target_arch = "wasm32"))]
6858 eprintln!("WARNING: {message}");
6859 return numeric_literal_at_source_range(ast, source_range).map(Some);
6860 };
6861 let find = FindSketchVarInitialByNodePath {
6862 target: node_path,
6863 sketch_var_found: Cell::new(false),
6864 initial_literal: Cell::new(None),
6865 };
6866 let node = crate::walk::Node::from(ast);
6867 node.visit(&find).ok()?;
6868 if !find.sketch_var_found.get() {
6869 return None;
6870 }
6871 Some(find.initial_literal.into_inner())
6872}
6873
6874fn suffix_length_unit(suffix: NumericSuffix) -> Option<UnitLength> {
6875 match suffix {
6876 NumericSuffix::Mm => Some(UnitLength::Millimeters),
6877 NumericSuffix::Cm => Some(UnitLength::Centimeters),
6878 NumericSuffix::M => Some(UnitLength::Meters),
6879 NumericSuffix::Inch => Some(UnitLength::Inches),
6880 NumericSuffix::Ft => Some(UnitLength::Feet),
6881 NumericSuffix::Yd => Some(UnitLength::Yards),
6882 _ => None,
6883 }
6884}
6885
6886fn number_value_in_default_length_units(number: Number, default_length_unit: UnitLength) -> f64 {
6887 match suffix_length_unit(number.units) {
6888 Some(unit) => adjust_length(unit, number.value, default_length_unit).0,
6889 None => number.value,
6890 }
6891}
6892
6893fn literal_value_in_default_length_units(literal: &ast::NumericLiteral, default_length_unit: UnitLength) -> f64 {
6894 match suffix_length_unit(literal.suffix) {
6895 Some(unit) => adjust_length(unit, literal.value, default_length_unit).0,
6896 None => literal.value,
6897 }
6898}
6899
6900fn var_solution_needs_commit(
6901 current_literal: &ast::NumericLiteral,
6902 solved_value: Number,
6903 default_length_unit: UnitLength,
6904) -> bool {
6905 let current = literal_value_in_default_length_units(current_literal, default_length_unit);
6906 let solved = number_value_in_default_length_units(solved_value, default_length_unit);
6907
6908 (current - solved).abs() > 1e-9
6909}
6910
6911fn preserve_var_solution_literal_style(
6912 current_literal: &ast::NumericLiteral,
6913 solved_value: Number,
6914 default_length_unit: UnitLength,
6915) -> Number {
6916 if current_literal.suffix == NumericSuffix::None {
6917 return Number {
6918 value: number_value_in_default_length_units(solved_value, default_length_unit),
6919 units: NumericSuffix::None,
6920 };
6921 }
6922
6923 let Some(current_unit) = suffix_length_unit(current_literal.suffix) else {
6924 return solved_value;
6925 };
6926
6927 let solved_default_value = number_value_in_default_length_units(solved_value, default_length_unit);
6928 Number {
6929 value: adjust_length(default_length_unit, solved_default_value, current_unit).0,
6930 units: current_literal.suffix,
6931 }
6932}
6933
6934pub(crate) fn to_ast_point2d(point: &Point2d<Expr>) -> anyhow::Result<ast::Expr> {
6935 Ok(ast::Expr::ArrayExpression(BoxNode::new(ast::Node {
6936 inner: ast::ArrayExpression {
6937 elements: vec![to_source_expr(&point.x)?, to_source_expr(&point.y)?],
6938 non_code_meta: Default::default(),
6939 digest: None,
6940 },
6941 start: Default::default(),
6942 end: Default::default(),
6943 module_id: Default::default(),
6944 node_path: None,
6945 outer_attrs: Default::default(),
6946 pre_comments: Default::default(),
6947 comment_start: Default::default(),
6948 })))
6949}
6950
6951pub(crate) fn to_ast_point2d_array(points: &[Point2d<Expr>]) -> anyhow::Result<ast::Expr> {
6952 Ok(ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(
6953 ast::ArrayExpression {
6954 elements: points.iter().map(to_ast_point2d).collect::<anyhow::Result<Vec<_>>>()?,
6955 digest: None,
6956 non_code_meta: Default::default(),
6957 },
6958 ))))
6959}
6960
6961fn to_ast_point2d_number(point: &Point2d<Number>) -> anyhow::Result<ast::Expr> {
6962 Ok(ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(
6963 ast::ArrayExpression {
6964 elements: vec![
6965 ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal::from(to_source_number(
6966 point.x,
6967 )?)))),
6968 ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal::from(to_source_number(
6969 point.y,
6970 )?)))),
6971 ],
6972 non_code_meta: Default::default(),
6973 digest: None,
6974 },
6975 ))))
6976}
6977
6978fn to_source_expr(expr: &Expr) -> anyhow::Result<ast::Expr> {
6979 match expr {
6980 Expr::Number(number) => Ok(ast::Expr::Literal(BoxNode::new(ast::Node {
6981 inner: ast::Literal::from(to_source_number(*number)?),
6982 start: Default::default(),
6983 end: Default::default(),
6984 module_id: Default::default(),
6985 node_path: None,
6986 outer_attrs: Default::default(),
6987 pre_comments: Default::default(),
6988 comment_start: Default::default(),
6989 }))),
6990 Expr::Var(number) => Ok(ast::Expr::SketchVar(BoxNode::new(ast::Node {
6991 inner: ast::SketchVar {
6992 initial: Some(BoxNode::new(ast::Node {
6993 inner: to_source_number(*number)?,
6994 start: Default::default(),
6995 end: Default::default(),
6996 module_id: Default::default(),
6997 node_path: None,
6998 outer_attrs: Default::default(),
6999 pre_comments: Default::default(),
7000 comment_start: Default::default(),
7001 })),
7002 digest: None,
7003 },
7004 start: Default::default(),
7005 end: Default::default(),
7006 module_id: Default::default(),
7007 node_path: None,
7008 outer_attrs: Default::default(),
7009 pre_comments: Default::default(),
7010 comment_start: Default::default(),
7011 }))),
7012 Expr::Variable(variable) => Ok(ast_name_expr(variable.clone())),
7013 }
7014}
7015
7016fn to_source_number(number: Number) -> anyhow::Result<ast::NumericLiteral> {
7017 Ok(ast::NumericLiteral {
7018 value: number.value,
7019 suffix: number.units,
7020 raw: format_number_literal(number.value, number.units, None)?,
7021 digest: None,
7022 })
7023}
7024
7025pub(crate) fn ast_name_expr(name: String) -> ast::Expr {
7026 ast::Expr::Name(BoxNode::new(ast_name(name)))
7027}
7028
7029fn ast_name(name: String) -> ast::Node<ast::Name> {
7030 ast::Node {
7031 inner: ast::Name {
7032 name: ast::Node {
7033 inner: ast::Identifier { name, digest: None },
7034 start: Default::default(),
7035 end: Default::default(),
7036 module_id: Default::default(),
7037 node_path: None,
7038 outer_attrs: Default::default(),
7039 pre_comments: Default::default(),
7040 comment_start: Default::default(),
7041 },
7042 path: Vec::new(),
7043 abs_path: false,
7044 digest: None,
7045 },
7046 start: Default::default(),
7047 end: Default::default(),
7048 module_id: Default::default(),
7049 node_path: None,
7050 outer_attrs: Default::default(),
7051 pre_comments: Default::default(),
7052 comment_start: Default::default(),
7053 }
7054}
7055
7056pub(crate) fn ast_sketch2_name(name: &str) -> ast::Name {
7057 ast::Name {
7058 name: ast::Node {
7059 inner: ast::Identifier {
7060 name: name.to_owned(),
7061 digest: None,
7062 },
7063 start: Default::default(),
7064 end: Default::default(),
7065 module_id: Default::default(),
7066 node_path: None,
7067 outer_attrs: Default::default(),
7068 pre_comments: Default::default(),
7069 comment_start: Default::default(),
7070 },
7071 path: Default::default(),
7072 abs_path: false,
7073 digest: None,
7074 }
7075}
7076
7077pub(crate) fn create_coincident_ast(exprs: impl IntoIterator<Item = ast::Expr>) -> ast::Expr {
7079 let elements = exprs.into_iter().collect::<Vec<_>>();
7080 debug_assert!(elements.len() >= 2, "Coincident AST should have at least 2 inputs");
7081
7082 let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7084 elements,
7085 digest: None,
7086 non_code_meta: Default::default(),
7087 })));
7088
7089 ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7091 callee: ast::Node::no_src(ast_sketch2_name(COINCIDENT_FN)),
7092 unlabeled: Some(array_expr),
7093 arguments: Default::default(),
7094 digest: None,
7095 non_code_meta: Default::default(),
7096 })))
7097}
7098
7099pub(crate) fn create_horizontal_ast(line_expr: ast::Expr) -> ast::Expr {
7101 ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7102 callee: ast::Node::no_src(ast_sketch2_name(HORIZONTAL_FN)),
7103 unlabeled: Some(line_expr),
7104 arguments: Default::default(),
7105 digest: None,
7106 non_code_meta: Default::default(),
7107 })))
7108}
7109
7110pub(crate) fn create_vertical_ast(line_expr: ast::Expr) -> ast::Expr {
7112 ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7113 callee: ast::Node::no_src(ast_sketch2_name(VERTICAL_FN)),
7114 unlabeled: Some(line_expr),
7115 arguments: Default::default(),
7116 digest: None,
7117 non_code_meta: Default::default(),
7118 })))
7119}
7120
7121pub(crate) fn create_member_expression(object_expr: ast::Expr, property: &str) -> ast::Expr {
7123 ast::Expr::MemberExpression(BoxNode::new(ast::Node::no_src(ast::MemberExpression {
7124 object: object_expr,
7125 property: ast::Expr::Name(BoxNode::new(ast::Node::no_src(ast::Name {
7126 name: ast::Node::no_src(ast::Identifier {
7127 name: property.to_string(),
7128 digest: None,
7129 }),
7130 path: Vec::new(),
7131 abs_path: false,
7132 digest: None,
7133 }))),
7134 computed: false,
7135 digest: None,
7136 })))
7137}
7138
7139pub(crate) fn create_index_expression(object_expr: ast::Expr, index: usize) -> ast::Expr {
7140 ast::Expr::MemberExpression(BoxNode::new(ast::Node::no_src(ast::MemberExpression {
7141 object: object_expr,
7142 property: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal::from(
7143 ast::NumericLiteral {
7144 value: index as f64,
7145 suffix: NumericSuffix::None,
7146 raw: index.to_string(),
7147 digest: None,
7148 },
7149 )))),
7150 computed: true,
7151 digest: None,
7152 })))
7153}
7154
7155fn create_fixed_point_constraint_ast(point_expr: ast::Expr, position: Point2d<Number>) -> anyhow::Result<ast::Expr> {
7157 let x_literal = ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal::from(to_source_number(
7159 position.x,
7160 )?))));
7161 let y_literal = ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal::from(to_source_number(
7162 position.y,
7163 )?))));
7164 let point_array = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7165 elements: vec![x_literal, y_literal],
7166 digest: None,
7167 non_code_meta: Default::default(),
7168 })));
7169
7170 let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7172 elements: vec![point_expr, point_array],
7173 digest: None,
7174 non_code_meta: Default::default(),
7175 })));
7176
7177 Ok(ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(
7179 ast::CallExpressionKw {
7180 callee: ast::Node::no_src(ast_sketch2_name(FIXED_FN)),
7181 unlabeled: Some(array_expr),
7182 arguments: Default::default(),
7183 digest: None,
7184 non_code_meta: Default::default(),
7185 },
7186 ))))
7187}
7188
7189pub(crate) fn create_equal_length_ast(line_exprs: Vec<ast::Expr>) -> ast::Expr {
7191 let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7192 elements: line_exprs,
7193 digest: None,
7194 non_code_meta: Default::default(),
7195 })));
7196
7197 ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7199 callee: ast::Node::no_src(ast_sketch2_name(EQUAL_LENGTH_FN)),
7200 unlabeled: Some(array_expr),
7201 arguments: Default::default(),
7202 digest: None,
7203 non_code_meta: Default::default(),
7204 })))
7205}
7206
7207pub(crate) fn create_equal_radius_ast(segment_exprs: Vec<ast::Expr>) -> ast::Expr {
7209 let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7210 elements: segment_exprs,
7211 digest: None,
7212 non_code_meta: Default::default(),
7213 })));
7214
7215 ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7216 callee: ast::Node::no_src(ast_sketch2_name(EQUAL_RADIUS_FN)),
7217 unlabeled: Some(array_expr),
7218 arguments: Default::default(),
7219 digest: None,
7220 non_code_meta: Default::default(),
7221 })))
7222}
7223
7224pub(crate) fn create_tangent_ast(seg1_expr: ast::Expr, seg2_expr: ast::Expr) -> ast::Expr {
7226 let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7227 elements: vec![seg1_expr, seg2_expr],
7228 digest: None,
7229 non_code_meta: Default::default(),
7230 })));
7231
7232 ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7233 callee: ast::Node::no_src(ast_sketch2_name(TANGENT_FN)),
7234 unlabeled: Some(array_expr),
7235 arguments: Default::default(),
7236 digest: None,
7237 non_code_meta: Default::default(),
7238 })))
7239}
7240
7241pub(crate) fn create_symmetric_ast(input_exprs: Vec<ast::Expr>, axis_expr: ast::Expr) -> ast::Expr {
7243 let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7244 elements: input_exprs,
7245 digest: None,
7246 non_code_meta: Default::default(),
7247 })));
7248 let arguments = vec![ast::LabeledArg {
7249 label: Some(ast::Identifier::new(SYMMETRIC_AXIS_PARAM)),
7250 arg: axis_expr,
7251 }];
7252
7253 ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7254 callee: ast::Node::no_src(ast_sketch2_name(SYMMETRIC_FN)),
7255 unlabeled: Some(array_expr),
7256 arguments,
7257 digest: None,
7258 non_code_meta: Default::default(),
7259 })))
7260}
7261
7262pub(crate) fn create_midpoint_ast(segment_expr: ast::Expr, point_expr: ast::Expr) -> ast::Expr {
7264 let arguments = vec![ast::LabeledArg {
7265 label: Some(ast::Identifier::new(MIDPOINT_POINT_PARAM)),
7266 arg: point_expr,
7267 }];
7268
7269 ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7270 callee: ast::Node::no_src(ast_sketch2_name(MIDPOINT_FN)),
7271 unlabeled: Some(segment_expr),
7272 arguments,
7273 digest: None,
7274 non_code_meta: Default::default(),
7275 })))
7276}
7277
7278fn issue_source_range(error: &KclError) -> SourceRange {
7283 let source_ranges = error.source_ranges();
7284 source_ranges
7285 .iter()
7286 .find(|range| range.is_top_level_module())
7287 .or_else(|| source_ranges.first())
7288 .copied()
7289 .unwrap_or_else(SourceRange::synthetic)
7290}
7291
7292#[cfg(test)]
7293mod tests {
7294 use std::sync;
7295
7296 use kcl_api::Distance;
7297 use kcl_api::Fixed;
7298 use kcl_api::FixedPoint;
7299 use kcl_api::Midpoint;
7300 use kcl_api::NumericSuffix;
7301 use kcl_api::Object;
7302 use kcl_api::Plane;
7303 use kcl_api::Sketch;
7304 use kcl_api::Tangent;
7305 use kcl_api::Vertical;
7306
7307 use super::*;
7308 use crate::engine::PlaneName;
7309 use crate::engine::engine_manager::EngineManager;
7310 use crate::execution::cache::SketchModeState;
7311
7312 fn find_first_sketch_object(scene_graph: &SceneGraph) -> Option<&Object> {
7313 for object in &scene_graph.objects {
7314 if let ObjectKind::Sketch(_) = &object.kind {
7315 return Some(object);
7316 }
7317 }
7318 None
7319 }
7320
7321 fn find_first_face_object(scene_graph: &SceneGraph) -> Option<&Object> {
7322 for object in &scene_graph.objects {
7323 if let ObjectKind::Face(_) = &object.kind {
7324 return Some(object);
7325 }
7326 }
7327 None
7328 }
7329
7330 fn find_first_wall_object_id(scene_graph: &SceneGraph) -> Option<ObjectId> {
7331 for object in &scene_graph.objects {
7332 if matches!(&object.kind, ObjectKind::Wall(_)) {
7333 return Some(object.id);
7334 }
7335 }
7336 None
7337 }
7338
7339 fn find_cap_object_id_with_solid_output_index(
7340 scene_graph: &SceneGraph,
7341 cap_kind: kcl_api::CapKind,
7342 solid_output_index: usize,
7343 ) -> Option<ObjectId> {
7344 for object in &scene_graph.objects {
7345 if matches!(&object.kind, ObjectKind::Cap(cap) if cap.kind == cap_kind && cap.solid_output_index == Some(solid_output_index))
7346 {
7347 return Some(object.id);
7348 }
7349 }
7350 None
7351 }
7352
7353 #[test]
7354 fn issue_source_range_prefers_top_level_module() {
7355 use kcl_error::ModuleId;
7356
7357 let top = SourceRange::new(10, 20, ModuleId::default());
7358 let imported = SourceRange::new(0, 5, ModuleId::from_usize(7));
7359
7360 let error = KclError::new_semantic(crate::errors::KclErrorDetails::new(
7363 "boom".to_owned(),
7364 vec![imported, top],
7365 ));
7366 assert_eq!(super::issue_source_range(&error), top);
7367
7368 let error = KclError::new_semantic(crate::errors::KclErrorDetails::new("boom".to_owned(), vec![imported]));
7370 assert_eq!(super::issue_source_range(&error), imported);
7371
7372 let error = KclError::new_semantic(crate::errors::KclErrorDetails::new("boom".to_owned(), vec![]));
7374 assert_eq!(super::issue_source_range(&error), SourceRange::synthetic());
7375 }
7376
7377 #[test]
7378 fn composite_constituent_sweeps_are_not_solid_outputs() {
7379 use kcl_api::artifact::ArtifactSweepMethod;
7380 use kcl_api::artifact::CompositeSolid;
7381 use kcl_api::artifact::CompositeSolidSubType;
7382 use kcl_api::artifact::Sweep;
7383 use kcl_api::artifact::SweepSubType;
7384
7385 let first_sweep_id = ArtifactId::new(Uuid::new_v4());
7386 let second_sweep_id = ArtifactId::new(Uuid::new_v4());
7387 let composite_id = ArtifactId::new(Uuid::new_v4());
7388 let code_ref = CodeRef::placeholder(SourceRange::synthetic());
7389 let sweep = |id| {
7390 Artifact::Sweep(Sweep {
7391 id,
7392 sub_type: SweepSubType::Extrusion,
7393 path_id: Some(ArtifactId::new(Uuid::new_v4())),
7394 surface_ids: Vec::new(),
7395 edge_ids: Vec::new(),
7396 code_ref: code_ref.clone(),
7397 source_sweep_id: None,
7398 trajectory_id: None,
7399 method: ArtifactSweepMethod::New,
7400 consumed: false,
7401 pattern_ids: Vec::new(),
7402 })
7403 };
7404 let mut artifacts = IndexMap::from([
7405 (first_sweep_id, sweep(first_sweep_id)),
7406 (second_sweep_id, sweep(second_sweep_id)),
7407 ]);
7408
7409 let top_level_graph = ArtifactGraph::from_parts(artifacts.clone(), artifacts.len());
7410 assert_eq!(
7411 solid_output_index_for_sweep(&top_level_graph, first_sweep_id, &code_ref),
7412 Some(0)
7413 );
7414 assert_eq!(
7415 solid_output_index_for_sweep(&top_level_graph, second_sweep_id, &code_ref),
7416 Some(1)
7417 );
7418
7419 artifacts.insert(
7420 composite_id,
7421 Artifact::CompositeSolid(CompositeSolid {
7422 id: composite_id,
7423 consumed: false,
7424 sub_type: CompositeSolidSubType::Union,
7425 output_index: None,
7426 solid_ids: vec![first_sweep_id, second_sweep_id],
7427 tool_ids: Vec::new(),
7428 code_ref,
7429 composite_solid_id: None,
7430 pattern_ids: Vec::new(),
7431 }),
7432 );
7433 let composite_graph = ArtifactGraph::from_parts(artifacts.clone(), artifacts.len());
7434 assert_eq!(
7435 solid_output_index_for_sweep(&composite_graph, first_sweep_id, &CodeRef::default()),
7436 None
7437 );
7438 assert_eq!(
7439 solid_output_index_for_sweep(&composite_graph, second_sweep_id, &CodeRef::default()),
7440 None
7441 );
7442 }
7443
7444 #[test]
7445 fn test_region_name_from_sweep_variable_supports_sweep_kinds() {
7446 let source = "\
7447region001 = region(point = [0.1, 0.1], sketch = s)
7448extrude001 = extrude(region001, length = 5)
7449revolve001 = revolve(region001, axis = Y)
7450sweep001 = sweep(region001, path = path001)
7451loft001 = loft(region001)
7452not_sweep001 = shell(extrude001, faces = [], thickness = 1)
7453";
7454
7455 let program = Program::parse(source).unwrap().0.unwrap();
7456
7457 assert_eq!(
7458 region_name_from_sweep_variable(&program.ast, "extrude001"),
7459 Some("region001".to_owned())
7460 );
7461 assert_eq!(
7462 region_name_from_sweep_variable(&program.ast, "revolve001"),
7463 Some("region001".to_owned())
7464 );
7465 assert_eq!(
7466 region_name_from_sweep_variable(&program.ast, "sweep001"),
7467 Some("region001".to_owned())
7468 );
7469 assert_eq!(
7470 region_name_from_sweep_variable(&program.ast, "loft001"),
7471 Some("region001".to_owned())
7472 );
7473 assert_eq!(region_name_from_sweep_variable(&program.ast, "not_sweep001"), None);
7474 }
7475
7476 #[track_caller]
7477 fn expect_sketch(object: &Object) -> &Sketch {
7478 if let ObjectKind::Sketch(sketch) = &object.kind {
7479 sketch
7480 } else {
7481 panic!("Object is not a sketch: {:?}", object);
7482 }
7483 }
7484
7485 fn point_position(scene_graph: &SceneGraph, point_id: ObjectId) -> Point2d<Number> {
7486 let point_object = scene_graph.objects.get(point_id.0).unwrap();
7487 let ObjectKind::Segment {
7488 segment: Segment::Point(point),
7489 } = &point_object.kind
7490 else {
7491 panic!("Object is not a point segment: {point_object:?}");
7492 };
7493 point.position.clone()
7494 }
7495
7496 fn assert_point_position_close(actual: Point2d<Number>, expected: Point2d<Number>) {
7497 assert!((actual.x.value - expected.x.value).abs() < 1e-6);
7498 assert!((actual.y.value - expected.y.value).abs() < 1e-6);
7499 }
7500
7501 fn point_expr_mm(x: f64, y: f64) -> Point2d<Expr> {
7504 Point2d {
7505 x: Expr::Var(Number {
7506 value: x,
7507 units: NumericSuffix::Mm,
7508 }),
7509 y: Expr::Var(Number {
7510 value: y,
7511 units: NumericSuffix::Mm,
7512 }),
7513 }
7514 }
7515
7516 fn point_number_mm(x: f64, y: f64) -> Point2d<Number> {
7519 Point2d {
7520 x: Number {
7521 value: x,
7522 units: NumericSuffix::Mm,
7523 },
7524 y: Number {
7525 value: y,
7526 units: NumericSuffix::Mm,
7527 },
7528 }
7529 }
7530
7531 fn make_line_ctor(start_x: f64, start_y: f64, end_x: f64, end_y: f64, units: NumericSuffix) -> LineCtor {
7532 LineCtor {
7533 start: Point2d {
7534 x: Expr::Number(Number { value: start_x, units }),
7535 y: Expr::Number(Number { value: start_y, units }),
7536 },
7537 end: Point2d {
7538 x: Expr::Number(Number { value: end_x, units }),
7539 y: Expr::Number(Number { value: end_y, units }),
7540 },
7541 construction: None,
7542 }
7543 }
7544
7545 async fn create_sketch_with_single_line(
7546 frontend: &mut FrontendState,
7547 ctx: &ExecutorContext,
7548 mock_ctx: &ExecutorContext,
7549 version: Version,
7550 ) -> (ObjectId, ObjectId, SourceDelta, SceneGraphDelta) {
7551 frontend.program = Program::empty();
7552
7553 let sketch_args = SketchCtor {
7554 on: Plane::Default(PlaneName::Xy),
7555 };
7556 let (_src_delta, _scene_delta, sketch_id) = frontend
7557 .new_sketch(ctx, ProjectId(0), FileId(0), version, sketch_args)
7558 .await
7559 .unwrap();
7560
7561 let segment = SegmentCtor::Line(make_line_ctor(0.0, 0.0, 10.0, 10.0, NumericSuffix::Mm));
7562 let (source_delta, scene_graph_delta) = frontend
7563 .add_segment(mock_ctx, version, sketch_id, segment, None)
7564 .await
7565 .unwrap();
7566 let line_id = *scene_graph_delta
7567 .new_objects
7568 .last()
7569 .expect("Expected line object id to be created");
7570
7571 (sketch_id, line_id, source_delta, scene_graph_delta)
7572 }
7573
7574 async fn seed_frontend_with_mock(frontend: &mut FrontendState, mock_ctx: &ExecutorContext, program: &Program) {
7575 frontend.program = program.clone();
7576 let outcome = mock_ctx.run_mock(program, &MockConfig::default()).await.unwrap();
7577 frontend.update_state_after_exec(outcome, true);
7578 }
7579
7580 #[test]
7581 fn test_parse_frontend_mutation_source_error_messages_are_user_facing() {
7582 for (source, expected_message) in [
7583 ("**", "Error parsing KCL source after editing: Unexpected token: *"),
7584 (
7585 "3'",
7586 "Error parsing KCL source after editing: unterminated string literal",
7587 ),
7588 ] {
7589 let err = parse_frontend_mutation_source(
7590 source,
7591 "Error parsing KCL source after editing",
7592 "No AST produced after editing",
7593 )
7594 .expect_err("expected invalid KCL source to fail");
7595 let message = err.error.message();
7596
7597 assert_eq!(message, expected_message);
7598 assert!(!message.contains("CompilationIssue"));
7599 assert!(!message.contains("KclErrorDetails"));
7600 assert!(!message.contains("source_range"));
7601 }
7602 }
7603
7604 #[tokio::test(flavor = "multi_thread")]
7605 async fn test_edit_constraint_value_parse_error_messages_are_user_facing() {
7606 let initial_source = "\
7607sketch(on = XY) {
7608 line1 = line(start = [var 0, var 0], end = [var 10, var 0])
7609 distance([line1.start, line1.end]) == 10
7610}
7611";
7612 let program = Program::parse(initial_source).unwrap().0.unwrap();
7613
7614 let mut frontend = FrontendState::new();
7615 let mock_ctx = ExecutorContext::new_mock(None).await;
7616 let version = Version(0);
7617
7618 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
7619 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7620 let sketch_id = sketch_object.id;
7621 let sketch = expect_sketch(sketch_object);
7622 let constraint_id = *sketch.constraints.first().expect("expected distance constraint");
7623
7624 for (value, expected_message) in [
7625 ("**", "Invalid constraint value: Unexpected token: *"),
7626 ("3'", "Invalid constraint value: unterminated string literal"),
7627 ] {
7628 let err = frontend
7629 .edit_constraint_value(&mock_ctx, version, sketch_id, constraint_id, value.to_owned())
7630 .await
7631 .expect_err("expected invalid constraint expression to fail");
7632 let message = err.error.message();
7633
7634 assert_eq!(message, expected_message);
7635 assert!(!message.contains("CompilationIssue"));
7636 assert!(!message.contains("KclErrorDetails"));
7637 assert!(!message.contains("source_range"));
7638 }
7639
7640 mock_ctx.close().await;
7641 }
7642
7643 #[tokio::test(flavor = "multi_thread")]
7644 async fn test_failed_edit_constraint_value_does_not_update_program() {
7645 let initial_source = "\
7646sketch(on = XY) {
7647 line1 = line(start = [var 0, var 0], end = [var 10, var 0])
7648 distance([line1.start, line1.end]) == 10
7649}
7650";
7651 let program = Program::parse(initial_source).unwrap().0.unwrap();
7652 let original_source = program.original_file_contents.clone();
7653
7654 let mut frontend = FrontendState::new();
7655 let mock_ctx = ExecutorContext::new_mock(None).await;
7656 let version = Version(0);
7657
7658 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
7659 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7660 let sketch_id = sketch_object.id;
7661 let sketch = expect_sketch(sketch_object);
7662 let constraint_id = *sketch.constraints.first().expect("expected distance constraint");
7663
7664 frontend
7665 .edit_constraint_value(
7666 &mock_ctx,
7667 version,
7668 sketch_id,
7669 constraint_id,
7670 "unknownDistance".to_owned(),
7671 )
7672 .await
7673 .expect_err("expected invalid constraint value to fail execution");
7674
7675 assert_eq!(frontend.program.original_file_contents, original_source);
7676 assert!(!frontend.program.original_file_contents.contains("unknownDistance"));
7677
7678 mock_ctx.close().await;
7679 }
7680
7681 #[tokio::test(flavor = "multi_thread")]
7682 async fn test_edit_constraint_value_array_index_oob_fails_in_sketch_mode() {
7683 let initial_source = "\
7684arr = [0]
7685sketch(on = XY) {
7686 line1 = line(start = [var 0, var 0], end = [var 10, var 0])
7687 distance([line1.start, line1.end]) == 10
7688}
7689";
7690 let program = Program::parse(initial_source).unwrap().0.unwrap();
7691
7692 let mut frontend = FrontendState::new();
7693 let mock_ctx = ExecutorContext::new_mock(None).await;
7694 let version = Version(0);
7695
7696 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
7697 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7698 let sketch_id = sketch_object.id;
7699 let sketch = expect_sketch(sketch_object);
7700 let constraint_id = *sketch.constraints.first().expect("expected distance constraint");
7701
7702 let err = frontend
7706 .edit_constraint_value(&mock_ctx, version, sketch_id, constraint_id, "arr[5]".to_owned())
7707 .await
7708 .expect_err("expected out-of-bounds array index to be an error in sketch mode execution");
7709 let message = err.error.message();
7710 assert!(
7711 message.contains("The array doesn't have any item at index 5"),
7712 "unexpected error message: {message}"
7713 );
7714
7715 mock_ctx.close().await;
7716 }
7717
7718 #[tokio::test(flavor = "multi_thread")]
7719 async fn test_sketch_checkpoint_round_trip_restores_state() {
7720 let mut frontend = FrontendState::new();
7721 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
7722 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
7723 let version = Version(0);
7724
7725 let (sketch_id, line_id, source_delta, scene_graph_delta) =
7726 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7727
7728 let expected_source = source_delta.text.clone();
7729 let expected_scene_graph = frontend.scene_graph.clone();
7730 let expected_exec_outcome = scene_graph_delta.exec_outcome.clone();
7731 let expected_point_freedom_cache = frontend.point_freedom_cache.clone();
7732
7733 let checkpoint_id = frontend
7734 .create_sketch_checkpoint(&ctx.cache, scene_graph_delta.exec_outcome.clone())
7735 .await
7736 .unwrap();
7737
7738 let edited_segments = vec![ExistingSegmentCtor {
7739 id: line_id,
7740 ctor: SegmentCtor::Line(make_line_ctor(1.0, 2.0, 13.0, 14.0, NumericSuffix::Mm)),
7741 }];
7742 let (edited_source, _edited_scene) = frontend
7743 .edit_segments(&mock_ctx, version, sketch_id, edited_segments)
7744 .await
7745 .unwrap();
7746 assert_ne!(edited_source.text, expected_source);
7747
7748 let restored = frontend
7749 .restore_sketch_checkpoint(&ctx.cache, checkpoint_id)
7750 .await
7751 .unwrap();
7752
7753 assert_eq!(restored.source_delta.text, expected_source);
7754 assert_eq!(restored.scene_graph_delta.new_graph, expected_scene_graph);
7755 assert!(restored.scene_graph_delta.invalidates_ids);
7756 assert_eq!(restored.scene_graph_delta.exec_outcome, expected_exec_outcome);
7757 assert_eq!(frontend.scene_graph, expected_scene_graph);
7758 assert_eq!(frontend.point_freedom_cache, expected_point_freedom_cache);
7759
7760 ctx.close().await;
7761 }
7762
7763 #[tokio::test(flavor = "multi_thread")]
7764 async fn test_sketch_checkpoints_prune_oldest_entries() {
7765 let mut frontend = FrontendState::new();
7766 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
7767 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
7768 let version = Version(0);
7769
7770 let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
7771 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7772
7773 let mut checkpoint_ids = Vec::new();
7774 for _ in 0..(MAX_SKETCH_CHECKPOINTS + 3) {
7775 checkpoint_ids.push(
7776 frontend
7777 .create_sketch_checkpoint(&ctx.cache, scene_graph_delta.exec_outcome.clone())
7778 .await
7779 .unwrap(),
7780 );
7781 }
7782
7783 assert_eq!(frontend.sketch_checkpoints.len(), MAX_SKETCH_CHECKPOINTS);
7784 assert!(checkpoint_ids.windows(2).all(|ids| ids[0] < ids[1]));
7785
7786 let oldest_retained = checkpoint_ids[3];
7787 assert_eq!(
7788 frontend.sketch_checkpoints.front().map(|checkpoint| checkpoint.id),
7789 Some(oldest_retained)
7790 );
7791
7792 let evicted_restore = frontend.restore_sketch_checkpoint(&ctx.cache, checkpoint_ids[0]).await;
7793 assert!(evicted_restore.is_err());
7794 assert!(evicted_restore.unwrap_err().msg.contains("Sketch checkpoint not found"));
7795
7796 frontend
7797 .restore_sketch_checkpoint(&ctx.cache, *checkpoint_ids.last().unwrap())
7798 .await
7799 .unwrap();
7800
7801 ctx.close().await;
7802 }
7803
7804 #[tokio::test(flavor = "multi_thread")]
7805 async fn test_sketch_checkpoint_uses_explicit_session_cache() {
7806 let mut frontend = FrontendState::new();
7807 let ctx = ExecutorContext::new_with_engine(sync::Arc::new(EngineManager::new_mock()), Default::default());
7808 let other = ExecutorContext::new_mock(None).await;
7809 let program = Program::parse_no_errs("@settings(kclVersion = 3.0)\nx = 2").unwrap();
7810 let outcome = ctx.run_with_caching(program).await.unwrap();
7811 let checkpoint = frontend.create_sketch_checkpoint(&ctx.cache, outcome).await.unwrap();
7812 ctx.cache.clear_mem_cache().await;
7813 assert!(ctx.cache.read_old_memory_var("x").await.is_none());
7814 frontend
7815 .restore_sketch_checkpoint(&ctx.cache, checkpoint)
7816 .await
7817 .unwrap();
7818 assert!(ctx.cache.read_old_memory_var("x").await.is_some());
7819 assert!(other.cache.read_old_memory_var("x").await.is_none());
7820 ctx.close().await;
7821 other.close().await;
7822 }
7823
7824 #[tokio::test(flavor = "multi_thread")]
7825 async fn test_restore_sketch_checkpoint_missing_id_returns_error() {
7826 let mut frontend = FrontendState::new();
7827 let ctx = ExecutorContext::new_mock(None).await;
7828 let missing_checkpoint = SketchCheckpointId::new(999);
7829
7830 let err = frontend
7831 .restore_sketch_checkpoint(&ctx.cache, missing_checkpoint)
7832 .await
7833 .expect_err("Expected restore to fail for missing checkpoint");
7834
7835 assert!(err.msg.contains("Sketch checkpoint not found"));
7836 }
7837
7838 #[tokio::test(flavor = "multi_thread")]
7839 async fn test_clear_sketch_checkpoints_removes_all_restore_points() {
7840 let mut frontend = FrontendState::new();
7841 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
7842 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
7843 let version = Version(0);
7844
7845 let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
7846 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7847
7848 let checkpoint_a = frontend
7849 .create_sketch_checkpoint(&ctx.cache, scene_graph_delta.exec_outcome.clone())
7850 .await
7851 .unwrap();
7852 let checkpoint_b = frontend
7853 .create_sketch_checkpoint(&ctx.cache, scene_graph_delta.exec_outcome.clone())
7854 .await
7855 .unwrap();
7856 assert_eq!(frontend.sketch_checkpoints.len(), 2);
7857
7858 frontend.clear_sketch_checkpoints();
7859 assert!(frontend.sketch_checkpoints.is_empty());
7860 frontend
7861 .restore_sketch_checkpoint(&ctx.cache, checkpoint_a)
7862 .await
7863 .unwrap_err();
7864 frontend
7865 .restore_sketch_checkpoint(&ctx.cache, checkpoint_b)
7866 .await
7867 .unwrap_err();
7868
7869 ctx.close().await;
7870 }
7871
7872 #[tokio::test(flavor = "multi_thread")]
7873 async fn test_hack_set_program_keeps_old_checkpoints_and_adds_fresh_baseline() {
7874 let mut frontend = FrontendState::new();
7875 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
7876 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
7877 let version = Version(0);
7878
7879 let (_sketch_id, _line_id, source_delta, scene_graph_delta) =
7880 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7881 let old_source = source_delta.text.clone();
7882 let old_checkpoint = frontend
7883 .create_sketch_checkpoint(&ctx.cache, scene_graph_delta.exec_outcome.clone())
7884 .await
7885 .unwrap();
7886 let initial_checkpoint_count = frontend.sketch_checkpoints.len();
7887
7888 let new_program = Program::parse("sketch(on = XY) {\n point(at = [1mm, 2mm])\n}\n")
7889 .unwrap()
7890 .0
7891 .unwrap();
7892
7893 let result = frontend.hack_set_program(&ctx, new_program).await.unwrap();
7894 let SetProgramOutcome::Success {
7895 checkpoint_id: Some(new_checkpoint),
7896 ..
7897 } = result
7898 else {
7899 panic!("Expected Success with a fresh checkpoint baseline");
7900 };
7901
7902 assert_eq!(frontend.sketch_checkpoints.len(), initial_checkpoint_count + 1);
7903
7904 let old_restore = frontend
7905 .restore_sketch_checkpoint(&ctx.cache, old_checkpoint)
7906 .await
7907 .unwrap();
7908 assert_eq!(old_restore.source_delta.text, old_source);
7909
7910 let new_restore = frontend
7911 .restore_sketch_checkpoint(&ctx.cache, new_checkpoint)
7912 .await
7913 .unwrap();
7914 assert!(new_restore.source_delta.text.contains("point(at = [1mm, 2mm])"));
7915
7916 ctx.close().await;
7917 }
7918
7919 #[tokio::test(flavor = "multi_thread")]
7920 async fn test_hack_set_program_exec_failure_does_not_add_checkpoint() {
7921 let mut frontend = FrontendState::new();
7922 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
7923 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
7924 let version = Version(0);
7925
7926 let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
7927 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7928 let old_checkpoint = frontend
7929 .create_sketch_checkpoint(&ctx.cache, scene_graph_delta.exec_outcome.clone())
7930 .await
7931 .unwrap();
7932 let checkpoint_count_before = frontend.sketch_checkpoints.len();
7933
7934 let failing_program = Program::parse(
7935 "sketch(on = XY) {\n line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])\n}\n\nbad = missing_name\n",
7936 )
7937 .unwrap()
7938 .0
7939 .unwrap();
7940
7941 let result = frontend.hack_set_program(&ctx, failing_program).await.unwrap();
7942 assert!(matches!(result, SetProgramOutcome::ExecFailure { .. }));
7943 assert_eq!(frontend.sketch_checkpoints.len(), checkpoint_count_before);
7944 frontend
7945 .restore_sketch_checkpoint(&ctx.cache, old_checkpoint)
7946 .await
7947 .unwrap();
7948
7949 ctx.close().await;
7950 }
7951
7952 #[tokio::test(flavor = "multi_thread")]
7953 async fn test_restore_sketch_checkpoint_restores_and_clears_mock_memory() {
7954 let mut frontend = FrontendState::new();
7955 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
7956
7957 let program = Program::parse(
7958 "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",
7959 )
7960 .unwrap()
7961 .0
7962 .unwrap();
7963 let set_program_outcome = frontend.hack_set_program(&ctx, program).await.unwrap();
7964 let SetProgramOutcome::Success { exec_outcome, .. } = set_program_outcome else {
7965 panic!("Expected successful baseline program execution");
7966 };
7967
7968 ctx.cache.clear_mem_cache().await;
7969 assert!(ctx.cache.read_old_memory().await.is_none());
7970
7971 let checkpoint_without_mock_memory = frontend
7972 .create_sketch_checkpoint(&ctx.cache, (*exec_outcome).clone())
7973 .await
7974 .unwrap();
7975
7976 ctx.cache.write_old_memory(SketchModeState::new_for_tests()).await;
7977 assert!(ctx.cache.read_old_memory().await.is_some());
7978
7979 let checkpoint_with_mock_memory = frontend
7980 .create_sketch_checkpoint(&ctx.cache, (*exec_outcome).clone())
7981 .await
7982 .unwrap();
7983
7984 ctx.cache.clear_mem_cache().await;
7985 assert!(ctx.cache.read_old_memory().await.is_none());
7986
7987 frontend
7988 .restore_sketch_checkpoint(&ctx.cache, checkpoint_with_mock_memory)
7989 .await
7990 .unwrap();
7991 assert!(ctx.cache.read_old_memory().await.is_some());
7992
7993 frontend
7994 .restore_sketch_checkpoint(&ctx.cache, checkpoint_without_mock_memory)
7995 .await
7996 .unwrap();
7997 assert!(ctx.cache.read_old_memory().await.is_none());
7998
7999 ctx.close().await;
8000 }
8001
8002 #[tokio::test(flavor = "multi_thread")]
8003 async fn test_hack_set_program_exec_error_still_allows_edit_sketch() {
8004 let source = "\
8005sketch(on = XY) {
8006 line1 = line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])
8007}
8008
8009bad = missing_name
8010";
8011 let program = Program::parse(source).unwrap().0.unwrap();
8012
8013 let mut frontend = FrontendState::new();
8014
8015 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
8016 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
8017 let version = Version(0);
8018 let project_id = ProjectId(0);
8019 let file_id = FileId(0);
8020
8021 let SetProgramOutcome::ExecFailure { .. } = frontend.hack_set_program(&ctx, program).await.unwrap() else {
8022 panic!("Expected ExecFailure from hack_set_program due to syntax error in program");
8023 };
8024
8025 let sketch_id = frontend
8026 .scene_graph
8027 .objects
8028 .iter()
8029 .find_map(|obj| matches!(obj.kind, ObjectKind::Sketch(_)).then_some(obj.id))
8030 .expect("Expected sketch object from errored hack_set_program");
8031
8032 frontend
8033 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
8034 .await
8035 .unwrap();
8036
8037 ctx.close().await;
8038 mock_ctx.close().await;
8039 }
8040
8041 #[tokio::test(flavor = "multi_thread")]
8042 async fn test_new_sketch_add_point_edit_point() {
8043 let program = Program::empty();
8044
8045 let mut frontend = FrontendState::new();
8046 frontend.program = program;
8047
8048 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
8049 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
8050 let version = Version(0);
8051
8052 let sketch_args = SketchCtor {
8053 on: Plane::Default(PlaneName::Xy),
8054 };
8055 let (_src_delta, scene_delta, sketch_id) = frontend
8056 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
8057 .await
8058 .unwrap();
8059 assert_eq!(sketch_id, ObjectId(1));
8060 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
8061 let sketch_object = &scene_delta.new_graph.objects[1];
8062 assert_eq!(sketch_object.id, ObjectId(1));
8063 assert_eq!(
8064 sketch_object.kind,
8065 ObjectKind::Sketch(Sketch {
8066 args: SketchCtor {
8067 on: Plane::Default(PlaneName::Xy)
8068 },
8069 plane: ObjectId(0),
8070 segments: vec![],
8071 constraints: vec![],
8072 })
8073 );
8074 assert_eq!(scene_delta.new_graph.objects.len(), 2);
8075
8076 let point_ctor = PointCtor {
8077 position: Point2d {
8078 x: Expr::Number(Number {
8079 value: 1.0,
8080 units: NumericSuffix::Inch,
8081 }),
8082 y: Expr::Number(Number {
8083 value: 2.0,
8084 units: NumericSuffix::Inch,
8085 }),
8086 },
8087 };
8088 let segment = SegmentCtor::Point(point_ctor);
8089 let (src_delta, scene_delta) = frontend
8090 .add_segment(&mock_ctx, version, sketch_id, segment, None)
8091 .await
8092 .unwrap();
8093 insta::assert_snapshot!("test_new_sketch_add_point_edit_point_1", src_delta.text.as_str());
8094 assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
8095 assert_eq!(scene_delta.new_graph.objects.len(), 3);
8096 for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
8097 assert_eq!(scene_object.id.0, i);
8098 }
8099
8100 let point_id = *scene_delta.new_objects.last().unwrap();
8101
8102 let point_ctor = PointCtor {
8103 position: Point2d {
8104 x: Expr::Number(Number {
8105 value: 3.0,
8106 units: NumericSuffix::Inch,
8107 }),
8108 y: Expr::Number(Number {
8109 value: 4.0,
8110 units: NumericSuffix::Inch,
8111 }),
8112 },
8113 };
8114 let segments = vec![ExistingSegmentCtor {
8115 id: point_id,
8116 ctor: SegmentCtor::Point(point_ctor),
8117 }];
8118 let (src_delta, scene_delta) = frontend
8119 .edit_segments(&mock_ctx, version, sketch_id, segments)
8120 .await
8121 .unwrap();
8122 insta::assert_snapshot!("test_new_sketch_add_point_edit_point_2", src_delta.text.as_str());
8123 assert_eq!(scene_delta.new_objects, vec![]);
8124 assert_eq!(scene_delta.new_graph.objects.len(), 3);
8125
8126 ctx.close().await;
8127 mock_ctx.close().await;
8128 }
8129
8130 #[tokio::test(flavor = "multi_thread")]
8131 async fn test_new_sketch_add_line_edit_line() {
8132 let program = Program::empty();
8133
8134 let mut frontend = FrontendState::new();
8135 frontend.program = program;
8136
8137 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
8138 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
8139 let version = Version(0);
8140
8141 let sketch_args = SketchCtor {
8142 on: Plane::Default(PlaneName::Xy),
8143 };
8144 let (_src_delta, scene_delta, sketch_id) = frontend
8145 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
8146 .await
8147 .unwrap();
8148 assert_eq!(sketch_id, ObjectId(1));
8149 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
8150 let sketch_object = &scene_delta.new_graph.objects[1];
8151 assert_eq!(sketch_object.id, ObjectId(1));
8152 assert_eq!(
8153 sketch_object.kind,
8154 ObjectKind::Sketch(Sketch {
8155 args: SketchCtor {
8156 on: Plane::Default(PlaneName::Xy)
8157 },
8158 plane: ObjectId(0),
8159 segments: vec![],
8160 constraints: vec![],
8161 })
8162 );
8163 assert_eq!(scene_delta.new_graph.objects.len(), 2);
8164
8165 let line_ctor = LineCtor {
8166 start: Point2d {
8167 x: Expr::Number(Number {
8168 value: 0.0,
8169 units: NumericSuffix::Mm,
8170 }),
8171 y: Expr::Number(Number {
8172 value: 0.0,
8173 units: NumericSuffix::Mm,
8174 }),
8175 },
8176 end: Point2d {
8177 x: Expr::Number(Number {
8178 value: 10.0,
8179 units: NumericSuffix::Mm,
8180 }),
8181 y: Expr::Number(Number {
8182 value: 10.0,
8183 units: NumericSuffix::Mm,
8184 }),
8185 },
8186 construction: None,
8187 };
8188 let segment = SegmentCtor::Line(line_ctor);
8189 let (src_delta, scene_delta) = frontend
8190 .add_segment(&mock_ctx, version, sketch_id, segment, None)
8191 .await
8192 .unwrap();
8193 insta::assert_snapshot!("test_new_sketch_add_line_edit_line_1", src_delta.text.as_str());
8194 assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
8195 assert_eq!(scene_delta.new_graph.objects.len(), 5);
8196 for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
8197 assert_eq!(scene_object.id.0, i);
8198 }
8199
8200 let line = *scene_delta.new_objects.last().unwrap();
8202
8203 let line_ctor = LineCtor {
8204 start: Point2d {
8205 x: Expr::Number(Number {
8206 value: 1.0,
8207 units: NumericSuffix::Mm,
8208 }),
8209 y: Expr::Number(Number {
8210 value: 2.0,
8211 units: NumericSuffix::Mm,
8212 }),
8213 },
8214 end: Point2d {
8215 x: Expr::Number(Number {
8216 value: 13.0,
8217 units: NumericSuffix::Mm,
8218 }),
8219 y: Expr::Number(Number {
8220 value: 14.0,
8221 units: NumericSuffix::Mm,
8222 }),
8223 },
8224 construction: None,
8225 };
8226 let segments = vec![ExistingSegmentCtor {
8227 id: line,
8228 ctor: SegmentCtor::Line(line_ctor),
8229 }];
8230 let (src_delta, scene_delta) = frontend
8231 .edit_segments(&mock_ctx, version, sketch_id, segments)
8232 .await
8233 .unwrap();
8234 insta::assert_snapshot!("test_new_sketch_add_line_edit_line_2", src_delta.text.as_str());
8235 assert_eq!(scene_delta.new_objects, vec![]);
8236 assert_eq!(scene_delta.new_graph.objects.len(), 5);
8237
8238 ctx.close().await;
8239 mock_ctx.close().await;
8240 }
8241
8242 #[tokio::test(flavor = "multi_thread")]
8243 async fn test_new_sketch_add_arc_edit_arc() {
8244 let program = Program::empty();
8245
8246 let mut frontend = FrontendState::new();
8247 frontend.program = program;
8248
8249 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
8250 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
8251 let version = Version(0);
8252
8253 let sketch_args = SketchCtor {
8254 on: Plane::Default(PlaneName::Xy),
8255 };
8256 let (_src_delta, scene_delta, sketch_id) = frontend
8257 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
8258 .await
8259 .unwrap();
8260 assert_eq!(sketch_id, ObjectId(1));
8261 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
8262 let sketch_object = &scene_delta.new_graph.objects[1];
8263 assert_eq!(sketch_object.id, ObjectId(1));
8264 assert_eq!(
8265 sketch_object.kind,
8266 ObjectKind::Sketch(Sketch {
8267 args: SketchCtor {
8268 on: Plane::Default(PlaneName::Xy),
8269 },
8270 plane: ObjectId(0),
8271 segments: vec![],
8272 constraints: vec![],
8273 })
8274 );
8275 assert_eq!(scene_delta.new_graph.objects.len(), 2);
8276
8277 let arc_ctor = ArcCtor {
8278 start: Point2d {
8279 x: Expr::Var(Number {
8280 value: 0.0,
8281 units: NumericSuffix::Mm,
8282 }),
8283 y: Expr::Var(Number {
8284 value: 0.0,
8285 units: NumericSuffix::Mm,
8286 }),
8287 },
8288 end: Point2d {
8289 x: Expr::Var(Number {
8290 value: 10.0,
8291 units: NumericSuffix::Mm,
8292 }),
8293 y: Expr::Var(Number {
8294 value: 10.0,
8295 units: NumericSuffix::Mm,
8296 }),
8297 },
8298 center: Point2d {
8299 x: Expr::Var(Number {
8300 value: 10.0,
8301 units: NumericSuffix::Mm,
8302 }),
8303 y: Expr::Var(Number {
8304 value: 0.0,
8305 units: NumericSuffix::Mm,
8306 }),
8307 },
8308 direction: None,
8309 construction: None,
8310 };
8311 let segment = SegmentCtor::Arc(arc_ctor);
8312 let (src_delta, scene_delta) = frontend
8313 .add_segment(&mock_ctx, version, sketch_id, segment, None)
8314 .await
8315 .unwrap();
8316 insta::assert_snapshot!("test_new_sketch_add_arc_edit_arc_1", src_delta.text.as_str());
8317 assert_eq!(
8318 scene_delta.new_objects,
8319 vec![ObjectId(2), ObjectId(3), ObjectId(4), ObjectId(5)]
8320 );
8321 for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
8322 assert_eq!(scene_object.id.0, i);
8323 }
8324 assert_eq!(scene_delta.new_graph.objects.len(), 6);
8325
8326 let arc = *scene_delta.new_objects.last().unwrap();
8328
8329 let arc_ctor = ArcCtor {
8330 start: Point2d {
8331 x: Expr::Var(Number {
8332 value: 1.0,
8333 units: NumericSuffix::Mm,
8334 }),
8335 y: Expr::Var(Number {
8336 value: 2.0,
8337 units: NumericSuffix::Mm,
8338 }),
8339 },
8340 end: Point2d {
8341 x: Expr::Var(Number {
8342 value: 13.0,
8343 units: NumericSuffix::Mm,
8344 }),
8345 y: Expr::Var(Number {
8346 value: 14.0,
8347 units: NumericSuffix::Mm,
8348 }),
8349 },
8350 center: Point2d {
8351 x: Expr::Var(Number {
8352 value: 13.0,
8353 units: NumericSuffix::Mm,
8354 }),
8355 y: Expr::Var(Number {
8356 value: 2.0,
8357 units: NumericSuffix::Mm,
8358 }),
8359 },
8360 direction: None,
8361 construction: None,
8362 };
8363 let segments = vec![ExistingSegmentCtor {
8364 id: arc,
8365 ctor: SegmentCtor::Arc(arc_ctor),
8366 }];
8367 let (src_delta, scene_delta) = frontend
8368 .edit_segments(&mock_ctx, version, sketch_id, segments)
8369 .await
8370 .unwrap();
8371 insta::assert_snapshot!("test_new_sketch_add_arc_edit_arc_2", src_delta.text.as_str());
8372 assert_eq!(scene_delta.new_objects, vec![]);
8373 assert_eq!(scene_delta.new_graph.objects.len(), 6);
8374
8375 ctx.close().await;
8376 mock_ctx.close().await;
8377 }
8378
8379 #[tokio::test(flavor = "multi_thread")]
8380 async fn test_new_sketch_add_circle_edit_circle() {
8381 let program = Program::empty();
8382
8383 let mut frontend = FrontendState::new();
8384 frontend.program = program;
8385
8386 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
8387 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
8388 let version = Version(0);
8389
8390 let sketch_args = SketchCtor {
8391 on: Plane::Default(PlaneName::Xy),
8392 };
8393 let (_src_delta, _scene_delta, sketch_id) = frontend
8394 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
8395 .await
8396 .unwrap();
8397
8398 let circle_ctor = CircleCtor {
8400 start: Point2d {
8401 x: Expr::Var(Number {
8402 value: 5.0,
8403 units: NumericSuffix::Mm,
8404 }),
8405 y: Expr::Var(Number {
8406 value: 0.0,
8407 units: NumericSuffix::Mm,
8408 }),
8409 },
8410 center: Point2d {
8411 x: Expr::Var(Number {
8412 value: 0.0,
8413 units: NumericSuffix::Mm,
8414 }),
8415 y: Expr::Var(Number {
8416 value: 0.0,
8417 units: NumericSuffix::Mm,
8418 }),
8419 },
8420 construction: None,
8421 };
8422 let segment = SegmentCtor::Circle(circle_ctor);
8423 let (src_delta, scene_delta) = frontend
8424 .add_segment(&mock_ctx, version, sketch_id, segment, None)
8425 .await
8426 .unwrap();
8427 insta::assert_snapshot!("test_new_sketch_add_circle_edit_circle_1", src_delta.text.as_str());
8428 assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
8430 assert_eq!(scene_delta.new_graph.objects.len(), 5);
8431
8432 let circle = *scene_delta.new_objects.last().unwrap();
8433
8434 let circle_ctor = CircleCtor {
8436 start: Point2d {
8437 x: Expr::Var(Number {
8438 value: 10.0,
8439 units: NumericSuffix::Mm,
8440 }),
8441 y: Expr::Var(Number {
8442 value: 0.0,
8443 units: NumericSuffix::Mm,
8444 }),
8445 },
8446 center: Point2d {
8447 x: Expr::Var(Number {
8448 value: 3.0,
8449 units: NumericSuffix::Mm,
8450 }),
8451 y: Expr::Var(Number {
8452 value: 4.0,
8453 units: NumericSuffix::Mm,
8454 }),
8455 },
8456 construction: None,
8457 };
8458 let segments = vec![ExistingSegmentCtor {
8459 id: circle,
8460 ctor: SegmentCtor::Circle(circle_ctor),
8461 }];
8462 let (src_delta, scene_delta) = frontend
8463 .edit_segments(&mock_ctx, version, sketch_id, segments)
8464 .await
8465 .unwrap();
8466 insta::assert_snapshot!("test_new_sketch_add_circle_edit_circle_2", src_delta.text.as_str());
8467 assert_eq!(scene_delta.new_objects, vec![]);
8468 assert_eq!(scene_delta.new_graph.objects.len(), 5);
8469
8470 ctx.close().await;
8471 mock_ctx.close().await;
8472 }
8473
8474 #[tokio::test(flavor = "multi_thread")]
8475 async fn test_delete_circle() {
8476 let initial_source = "sketch001 = sketch(on = XY) {
8477 circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
8478}
8479";
8480
8481 let program = Program::parse(initial_source).unwrap().0.unwrap();
8482 let mut frontend = FrontendState::new();
8483
8484 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
8485 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
8486 let version = Version(0);
8487
8488 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8489 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8490 let sketch_id = sketch_object.id;
8491 let sketch = expect_sketch(sketch_object);
8492
8493 assert_eq!(sketch.segments.len(), 3);
8495 let circle_id = sketch.segments[2];
8496
8497 let (src_delta, scene_delta) = frontend
8499 .delete_objects(&mock_ctx, version, sketch_id, vec![], vec![circle_id])
8500 .await
8501 .unwrap();
8502 insta::assert_snapshot!("test_delete_circle", src_delta.text.as_str());
8503 let new_sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8504 let new_sketch = expect_sketch(new_sketch_object);
8505 assert_eq!(new_sketch.segments.len(), 0);
8506
8507 ctx.close().await;
8508 mock_ctx.close().await;
8509 }
8510
8511 #[tokio::test(flavor = "multi_thread")]
8512 async fn test_edit_circle_via_point() {
8513 let initial_source = "sketch001 = sketch(on = XY) {
8514 circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
8515}
8516";
8517
8518 let program = Program::parse(initial_source).unwrap().0.unwrap();
8519 let mut frontend = FrontendState::new();
8520
8521 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
8522 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
8523 let version = Version(0);
8524
8525 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8526 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8527 let sketch_id = sketch_object.id;
8528 let sketch = expect_sketch(sketch_object);
8529
8530 let circle_id = sketch
8532 .segments
8533 .iter()
8534 .copied()
8535 .find(|seg_id| {
8536 matches!(
8537 &frontend.scene_graph.objects[seg_id.0].kind,
8538 ObjectKind::Segment {
8539 segment: Segment::Circle(_)
8540 }
8541 )
8542 })
8543 .expect("Expected a circle segment in sketch");
8544 let circle_object = &frontend.scene_graph.objects[circle_id.0];
8545 let ObjectKind::Segment {
8546 segment: Segment::Circle(circle),
8547 } = &circle_object.kind
8548 else {
8549 panic!("Expected circle segment, got: {:?}", circle_object.kind);
8550 };
8551 let start_point_id = circle.start;
8552
8553 let segments = vec![ExistingSegmentCtor {
8555 id: start_point_id,
8556 ctor: SegmentCtor::Point(PointCtor {
8557 position: Point2d {
8558 x: Expr::Var(Number {
8559 value: 7.0,
8560 units: NumericSuffix::Mm,
8561 }),
8562 y: Expr::Var(Number {
8563 value: 1.0,
8564 units: NumericSuffix::Mm,
8565 }),
8566 },
8567 }),
8568 }];
8569 let (src_delta, _scene_delta) = frontend
8570 .edit_segments(&mock_ctx, version, sketch_id, segments)
8571 .await
8572 .unwrap();
8573 insta::assert_snapshot!("test_edit_circle_via_point", src_delta.text.as_str());
8574
8575 ctx.close().await;
8576 mock_ctx.close().await;
8577 }
8578
8579 #[tokio::test(flavor = "multi_thread")]
8580 async fn test_add_line_when_sketch_block_uses_variable() {
8581 let initial_source = "s = sketch(on = XY) {}
8582";
8583
8584 let program = Program::parse(initial_source).unwrap().0.unwrap();
8585
8586 let mut frontend = FrontendState::new();
8587
8588 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
8589 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
8590 let version = Version(0);
8591
8592 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8593 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8594 let sketch_id = sketch_object.id;
8595
8596 let line_ctor = LineCtor {
8597 start: Point2d {
8598 x: Expr::Number(Number {
8599 value: 0.0,
8600 units: NumericSuffix::Mm,
8601 }),
8602 y: Expr::Number(Number {
8603 value: 0.0,
8604 units: NumericSuffix::Mm,
8605 }),
8606 },
8607 end: Point2d {
8608 x: Expr::Number(Number {
8609 value: 10.0,
8610 units: NumericSuffix::Mm,
8611 }),
8612 y: Expr::Number(Number {
8613 value: 10.0,
8614 units: NumericSuffix::Mm,
8615 }),
8616 },
8617 construction: None,
8618 };
8619 let segment = SegmentCtor::Line(line_ctor);
8620 let (src_delta, scene_delta) = frontend
8621 .add_segment(&mock_ctx, version, sketch_id, segment, None)
8622 .await
8623 .unwrap();
8624 insta::assert_snapshot!("test_add_line_when_sketch_block_uses_variable", src_delta.text.as_str());
8625 assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
8626 assert_eq!(scene_delta.new_graph.objects.len(), 5);
8627
8628 ctx.close().await;
8629 mock_ctx.close().await;
8630 }
8631
8632 #[tokio::test(flavor = "multi_thread")]
8633 async fn test_new_sketch_add_line_delete_sketch() {
8634 let program = Program::empty();
8635
8636 let mut frontend = FrontendState::new();
8637 frontend.program = program;
8638
8639 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
8640 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
8641 let version = Version(0);
8642
8643 let sketch_args = SketchCtor {
8644 on: Plane::Default(PlaneName::Xy),
8645 };
8646 let (_src_delta, scene_delta, sketch_id) = frontend
8647 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
8648 .await
8649 .unwrap();
8650 assert_eq!(sketch_id, ObjectId(1));
8651 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
8652 let sketch_object = &scene_delta.new_graph.objects[1];
8653 assert_eq!(sketch_object.id, ObjectId(1));
8654 assert_eq!(
8655 sketch_object.kind,
8656 ObjectKind::Sketch(Sketch {
8657 args: SketchCtor {
8658 on: Plane::Default(PlaneName::Xy)
8659 },
8660 plane: ObjectId(0),
8661 segments: vec![],
8662 constraints: vec![],
8663 })
8664 );
8665 assert_eq!(scene_delta.new_graph.objects.len(), 2);
8666
8667 let line_ctor = LineCtor {
8668 start: Point2d {
8669 x: Expr::Number(Number {
8670 value: 0.0,
8671 units: NumericSuffix::Mm,
8672 }),
8673 y: Expr::Number(Number {
8674 value: 0.0,
8675 units: NumericSuffix::Mm,
8676 }),
8677 },
8678 end: Point2d {
8679 x: Expr::Number(Number {
8680 value: 10.0,
8681 units: NumericSuffix::Mm,
8682 }),
8683 y: Expr::Number(Number {
8684 value: 10.0,
8685 units: NumericSuffix::Mm,
8686 }),
8687 },
8688 construction: None,
8689 };
8690 let segment = SegmentCtor::Line(line_ctor);
8691 let (src_delta, scene_delta) = frontend
8692 .add_segment(&mock_ctx, version, sketch_id, segment, None)
8693 .await
8694 .unwrap();
8695 insta::assert_snapshot!("test_new_sketch_add_line_delete_sketch_1", src_delta.text.as_str());
8696 assert_eq!(scene_delta.new_graph.objects.len(), 5);
8697
8698 let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8699 insta::assert_snapshot!("test_new_sketch_add_line_delete_sketch_2", src_delta.text.as_str());
8700 assert_eq!(scene_delta.new_graph.objects.len(), 0);
8701
8702 ctx.close().await;
8703 mock_ctx.close().await;
8704 }
8705
8706 #[tokio::test(flavor = "multi_thread")]
8707 async fn test_delete_sketch_when_sketch_block_uses_variable() {
8708 let initial_source = "s = sketch(on = XY) {}
8709";
8710
8711 let program = Program::parse(initial_source).unwrap().0.unwrap();
8712
8713 let mut frontend = FrontendState::new();
8714
8715 let ctx = ExecutorContext::new_with_engine(sync::Arc::new(EngineManager::new_mock()), Default::default());
8716 let version = Version(0);
8717
8718 frontend.hack_set_program(&ctx, program).await.unwrap();
8719 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8720 let sketch_id = sketch_object.id;
8721
8722 let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8723 insta::assert_snapshot!(
8724 "test_delete_sketch_when_sketch_block_uses_variable",
8725 src_delta.text.as_str()
8726 );
8727 assert_eq!(scene_delta.new_graph.objects.len(), 0);
8728
8729 ctx.close().await;
8730 }
8731
8732 #[tokio::test(flavor = "multi_thread")]
8733 async fn test_delete_sketch_after_comment() {
8734 let initial_source = "sketch001 = sketch(on = XZ) {
8735}
8736";
8737
8738 let program = Program::parse(initial_source).unwrap().0.unwrap();
8739 let mut frontend = FrontendState::new();
8740
8741 let ctx = ExecutorContext::new_with_engine(sync::Arc::new(EngineManager::new_mock()), Default::default());
8742 let version = Version(0);
8743
8744 frontend.hack_set_program(&ctx, program).await.unwrap();
8745 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8746 let sketch_id = sketch_object.id;
8747 let original_source = sketch_object.source.clone();
8748
8749 let commented_source = "// test 1
8750sketch001 = sketch(on = XZ) {
8751}
8752";
8753 let commented_program = Program::parse(commented_source).unwrap().0.unwrap();
8754 frontend.engine_execute(&ctx, commented_program).await.unwrap();
8755
8756 let cached_sketch_object = &frontend.scene_graph.objects[sketch_id.0];
8757 assert_eq!(cached_sketch_object.source, original_source);
8758
8759 let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8760 assert!(
8761 !src_delta.text.contains("sketch001"),
8762 "sketch was not deleted: {}",
8763 src_delta.text
8764 );
8765 insta::assert_snapshot!("test_delete_sketch_after_comment", src_delta.text.as_str());
8767 assert_eq!(scene_delta.new_graph.objects.len(), 0);
8768
8769 ctx.close().await;
8770 }
8771
8772 #[tokio::test(flavor = "multi_thread")]
8773 async fn test_delete_sketch_preserves_pre_comment_when_followed_by_code() {
8774 let initial_source = "sketch001 = sketch(on = XZ) {
8775}
8776foo = 1
8777";
8778
8779 let program = Program::parse(initial_source).unwrap().0.unwrap();
8780 let mut frontend = FrontendState::new();
8781
8782 let ctx = ExecutorContext::new_with_engine(sync::Arc::new(EngineManager::new_mock()), Default::default());
8783 let version = Version(0);
8784
8785 frontend.hack_set_program(&ctx, program).await.unwrap();
8786 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8787 let sketch_id = sketch_object.id;
8788
8789 let commented_source = "// keep me
8790sketch001 = sketch(on = XZ) {
8791}
8792foo = 1
8793";
8794 let commented_program = Program::parse(commented_source).unwrap().0.unwrap();
8795 frontend.engine_execute(&ctx, commented_program).await.unwrap();
8796
8797 let (src_delta, _scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8798 insta::assert_snapshot!(
8800 "test_delete_sketch_preserves_pre_comment_when_followed_by_code",
8801 src_delta.text.as_str()
8802 );
8803
8804 ctx.close().await;
8805 }
8806
8807 #[tokio::test(flavor = "multi_thread")]
8808 async fn test_delete_segment_preserves_pre_comment() {
8809 let initial_source = "\
8810sketch(on = XY) {
8811 point(at = [var 1, var 2])
8812 // describe the middle point
8813 point(at = [var 3, var 4])
8814 point(at = [var 5, var 6])
8815}
8816";
8817
8818 let program = Program::parse(initial_source).unwrap().0.unwrap();
8819 let mut frontend = FrontendState::new();
8820
8821 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
8822 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
8823 let version = Version(0);
8824
8825 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8826 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8827 let sketch_id = sketch_object.id;
8828 let sketch = expect_sketch(sketch_object);
8829
8830 let middle_point_id = *sketch.segments.get(1).unwrap();
8831
8832 let (src_delta, _scene_delta) = frontend
8833 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![middle_point_id])
8834 .await
8835 .unwrap();
8836 insta::assert_snapshot!("test_delete_segment_preserves_pre_comment", src_delta.text.as_str());
8839
8840 ctx.close().await;
8841 mock_ctx.close().await;
8842 }
8843
8844 #[tokio::test(flavor = "multi_thread")]
8845 async fn test_delete_last_segment_preserves_pre_comment() {
8846 let initial_source = "\
8847sketch(on = XY) {
8848 point(at = [var 1, var 2])
8849 // describe the trailing point
8850 point(at = [var 3, var 4])
8851}
8852";
8853
8854 let program = Program::parse(initial_source).unwrap().0.unwrap();
8855 let mut frontend = FrontendState::new();
8856
8857 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
8858 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
8859 let version = Version(0);
8860
8861 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8862 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8863 let sketch_id = sketch_object.id;
8864 let sketch = expect_sketch(sketch_object);
8865
8866 let last_point_id = *sketch.segments.last().unwrap();
8867
8868 let (src_delta, _scene_delta) = frontend
8869 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![last_point_id])
8870 .await
8871 .unwrap();
8872 insta::assert_snapshot!(
8875 "test_delete_last_segment_preserves_pre_comment",
8876 src_delta.text.as_str()
8877 );
8878
8879 ctx.close().await;
8880 mock_ctx.close().await;
8881 }
8882
8883 #[tokio::test(flavor = "multi_thread")]
8884 async fn test_delete_segment_drops_inline_trailing_comment() {
8885 let initial_source = "\
8886sketch(on = XY) {
8887 point(at = [var 1, var 2])
8888 point(at = [var 3, var 4]) // same-line note that gets dropped
8889 point(at = [var 5, var 6])
8890}
8891";
8892
8893 let program = Program::parse(initial_source).unwrap().0.unwrap();
8894 let mut frontend = FrontendState::new();
8895
8896 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
8897 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
8898 let version = Version(0);
8899
8900 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8901 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8902 let sketch_id = sketch_object.id;
8903 let sketch = expect_sketch(sketch_object);
8904
8905 let middle_point_id = *sketch.segments.get(1).unwrap();
8906
8907 let (src_delta, _scene_delta) = frontend
8908 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![middle_point_id])
8909 .await
8910 .unwrap();
8911 assert!(
8913 !src_delta.text.contains("same-line note"),
8914 "inline comment should have been removed: {}",
8915 src_delta.text
8916 );
8917
8918 ctx.close().await;
8919 mock_ctx.close().await;
8920 }
8921
8922 #[tokio::test(flavor = "multi_thread")]
8923 async fn test_delete_segments_preserves_block_comments_across_positions() {
8924 let initial_source = "\
8932sketch(on = XY) {
8933 /* above first - moves to middle */
8934 point(at = [var 1, var 2]) /* same-line on first - dropped */
8935 /* above middle - stays */
8936 point(at = [var 3, var 4])
8937 /* above last - moves to trailing meta */
8938 point(at = [var 5, var 6])
8939}
8940";
8941
8942 let program = Program::parse(initial_source).unwrap().0.unwrap();
8943 let mut frontend = FrontendState::new();
8944
8945 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
8946 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
8947 let version = Version(0);
8948
8949 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8950 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8951 let sketch_id = sketch_object.id;
8952 let sketch = expect_sketch(sketch_object);
8953
8954 let first_point_id = *sketch.segments.first().unwrap();
8955 let last_point_id = *sketch.segments.last().unwrap();
8956
8957 let (src_delta, _scene_delta) = frontend
8958 .delete_objects(
8959 &mock_ctx,
8960 version,
8961 sketch_id,
8962 Vec::new(),
8963 vec![first_point_id, last_point_id],
8964 )
8965 .await
8966 .unwrap();
8967 insta::assert_snapshot!(
8968 "test_delete_segments_preserves_block_comments_across_positions",
8969 src_delta.text.as_str()
8970 );
8971
8972 ctx.close().await;
8973 mock_ctx.close().await;
8974 }
8975
8976 #[tokio::test(flavor = "multi_thread")]
8977 async fn test_edit_line_when_editing_its_start_point() {
8978 let initial_source = "\
8979sketch(on = XY) {
8980 line(start = [var 1, var 2], end = [var 3, var 4])
8981}
8982";
8983
8984 let program = Program::parse(initial_source).unwrap().0.unwrap();
8985
8986 let mut frontend = FrontendState::new();
8987
8988 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
8989 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
8990 let version = Version(0);
8991
8992 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8993 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8994 let sketch_id = sketch_object.id;
8995 let sketch = expect_sketch(sketch_object);
8996
8997 let point_id = *sketch.segments.first().unwrap();
8998
8999 let point_ctor = PointCtor {
9000 position: Point2d {
9001 x: Expr::Var(Number {
9002 value: 5.0,
9003 units: NumericSuffix::Inch,
9004 }),
9005 y: Expr::Var(Number {
9006 value: 6.0,
9007 units: NumericSuffix::Inch,
9008 }),
9009 },
9010 };
9011 let segments = vec![ExistingSegmentCtor {
9012 id: point_id,
9013 ctor: SegmentCtor::Point(point_ctor),
9014 }];
9015 let (src_delta, scene_delta) = frontend
9016 .edit_segments(&mock_ctx, version, sketch_id, segments)
9017 .await
9018 .unwrap();
9019 insta::assert_snapshot!("test_edit_line_when_editing_its_start_point", src_delta.text.as_str());
9020 assert_eq!(scene_delta.new_objects, vec![]);
9021 assert_eq!(scene_delta.new_graph.objects.len(), 5);
9022
9023 ctx.close().await;
9024 mock_ctx.close().await;
9025 }
9026
9027 #[tokio::test(flavor = "multi_thread")]
9028 async fn test_edit_line_when_editing_its_end_point() {
9029 let initial_source = "\
9030sketch(on = XY) {
9031 line(start = [var 1, var 2], end = [var 3, var 4])
9032}
9033";
9034
9035 let program = Program::parse(initial_source).unwrap().0.unwrap();
9036
9037 let mut frontend = FrontendState::new();
9038
9039 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
9040 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
9041 let version = Version(0);
9042
9043 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9044 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9045 let sketch_id = sketch_object.id;
9046 let sketch = expect_sketch(sketch_object);
9047 let point_id = *sketch.segments.get(1).unwrap();
9048
9049 let point_ctor = PointCtor {
9050 position: Point2d {
9051 x: Expr::Var(Number {
9052 value: 5.0,
9053 units: NumericSuffix::Inch,
9054 }),
9055 y: Expr::Var(Number {
9056 value: 6.0,
9057 units: NumericSuffix::Inch,
9058 }),
9059 },
9060 };
9061 let segments = vec![ExistingSegmentCtor {
9062 id: point_id,
9063 ctor: SegmentCtor::Point(point_ctor),
9064 }];
9065 let (src_delta, scene_delta) = frontend
9066 .edit_segments(&mock_ctx, version, sketch_id, segments)
9067 .await
9068 .unwrap();
9069 insta::assert_snapshot!("test_edit_line_when_editing_its_end_point", src_delta.text.as_str());
9070 assert_eq!(scene_delta.new_objects, vec![]);
9071 assert_eq!(
9072 scene_delta.new_graph.objects.len(),
9073 5,
9074 "{:#?}",
9075 scene_delta.new_graph.objects
9076 );
9077
9078 ctx.close().await;
9079 mock_ctx.close().await;
9080 }
9081
9082 #[tokio::test(flavor = "multi_thread")]
9083 async fn test_edit_line_with_coincident_feedback() {
9084 let initial_source = "\
9085sketch(on = XY) {
9086 line1 = line(start = [var 1, var 2], end = [var 1, var 2])
9087 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9088 fixed([line1.start, [0, 0]])
9089 coincident([line1.end, line2.start])
9090 equalLength([line1, line2])
9091}
9092";
9093
9094 let program = Program::parse(initial_source).unwrap().0.unwrap();
9095
9096 let mut frontend = FrontendState::new();
9097
9098 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
9099 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
9100 let version = Version(0);
9101
9102 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9103 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9104 let sketch_id = sketch_object.id;
9105 let sketch = expect_sketch(sketch_object);
9106 let line2_end_id = *sketch.segments.get(4).unwrap();
9107
9108 let segments = vec![ExistingSegmentCtor {
9109 id: line2_end_id,
9110 ctor: SegmentCtor::Point(PointCtor {
9111 position: Point2d {
9112 x: Expr::Var(Number {
9113 value: 9.0,
9114 units: NumericSuffix::None,
9115 }),
9116 y: Expr::Var(Number {
9117 value: 10.0,
9118 units: NumericSuffix::None,
9119 }),
9120 },
9121 }),
9122 }];
9123 let (src_delta, scene_delta) = frontend
9124 .edit_segments(&mock_ctx, version, sketch_id, segments)
9125 .await
9126 .unwrap();
9127 insta::assert_snapshot!("test_edit_line_with_coincident_feedback", src_delta.text.as_str());
9128 assert_eq!(
9129 scene_delta.new_graph.objects.len(),
9130 11,
9131 "{:#?}",
9132 scene_delta.new_graph.objects
9133 );
9134
9135 ctx.close().await;
9136 mock_ctx.close().await;
9137 }
9138
9139 #[tokio::test(flavor = "multi_thread")]
9140 async fn test_edit_segments_persists_solver_feedback_for_next_mock_execute() {
9141 let initial_source = "\
9142sketch(on = XY) {
9143 line1 = line(start = [var 1, var 2], end = [var 1, var 2])
9144 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9145 fixed([line1.start, [0, 0]])
9146 coincident([line1.end, line2.start])
9147 equalLength([line1, line2])
9148}
9149";
9150
9151 let program = Program::parse(initial_source).unwrap().0.unwrap();
9152 let mut frontend = FrontendState::new();
9153 let mock_ctx = ExecutorContext::new_mock(None).await;
9154 let version = Version(0);
9155
9156 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9157 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9158 let sketch_id = sketch_object.id;
9159 let sketch = expect_sketch(sketch_object);
9160 let line2_end_id = *sketch.segments.get(4).unwrap();
9161
9162 let segments = vec![ExistingSegmentCtor {
9163 id: line2_end_id,
9164 ctor: SegmentCtor::Point(PointCtor {
9165 position: Point2d {
9166 x: Expr::Var(Number {
9167 value: 9.0,
9168 units: NumericSuffix::None,
9169 }),
9170 y: Expr::Var(Number {
9171 value: 10.0,
9172 units: NumericSuffix::None,
9173 }),
9174 },
9175 }),
9176 }];
9177 let (edited_source, _) = frontend
9178 .edit_segments(&mock_ctx, version, sketch_id, segments)
9179 .await
9180 .unwrap();
9181
9182 let (mock_source, _) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
9183 assert_eq!(mock_source.text, edited_source.text);
9184
9185 mock_ctx.close().await;
9186 }
9187
9188 #[tokio::test(flavor = "multi_thread")]
9191 async fn test_preview_edit_segments_does_not_persist_solver_feedback() {
9192 let initial_source = "\
9193sketch(on = XY) {
9194 line1 = line(start = [var 1, var 2], end = [var 1, var 2])
9195 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9196 fixed([line1.start, [0, 0]])
9197 coincident([line1.end, line2.start])
9198 equalLength([line1, line2])
9199}
9200";
9201
9202 let program = Program::parse(initial_source).unwrap().0.unwrap();
9203 let mut frontend = FrontendState::new();
9204 let mock_ctx = ExecutorContext::new_mock(None).await;
9205 let version = Version(0);
9206
9207 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9208 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9209 let sketch_id = sketch_object.id;
9210 let sketch = expect_sketch(sketch_object);
9211 let line2_end_id = *sketch.segments.get(4).unwrap();
9212
9213 let segments = vec![ExistingSegmentCtor {
9214 id: line2_end_id,
9215 ctor: SegmentCtor::Point(PointCtor {
9216 position: Point2d {
9217 x: Expr::Var(Number {
9218 value: 9.0,
9219 units: NumericSuffix::None,
9220 }),
9221 y: Expr::Var(Number {
9222 value: 10.0,
9223 units: NumericSuffix::None,
9224 }),
9225 },
9226 }),
9227 }];
9228 let (preview_source, preview_delta) = frontend
9229 .edit_segments_with_options(
9230 &mock_ctx,
9231 version,
9232 sketch_id,
9233 segments,
9234 EditSegmentsOptions {
9235 anchor_segment_ids: Some(vec![line2_end_id]),
9236 drag_anchors: Vec::new(),
9237 constraint_label_edits: Vec::new(),
9238 commit_solved_initial_guesses: false,
9239 },
9240 )
9241 .await
9242 .unwrap();
9243
9244 assert!(
9245 !preview_delta.exec_outcome.var_solutions.is_empty(),
9246 "preview solve should still solve and return geometry feedback"
9247 );
9248 assert!(
9249 preview_source
9250 .text
9251 .contains("line1 = line(start = [var 1, var 2], end = [var 1, var 2])")
9252 );
9253 assert!(
9254 preview_source
9255 .text
9256 .contains("line2 = line(start = [var 5, var 6], end = [var 9, var 10])")
9257 );
9258
9259 let (mock_source, _) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
9260 assert_eq!(mock_source.text, preview_source.text);
9261
9262 mock_ctx.close().await;
9263 }
9264
9265 #[tokio::test(flavor = "multi_thread")]
9266 async fn test_add_constraint_persists_solver_feedback_for_next_mock_execute() {
9267 let initial_source = "\
9268sketch(on = XY) {
9269 line1 = line(start = [var 0, var 0], end = [var 10, var 0])
9270}
9271";
9272
9273 let program = Program::parse(initial_source).unwrap().0.unwrap();
9274 let mut frontend = FrontendState::new();
9275 let mock_ctx = ExecutorContext::new_mock(None).await;
9276 let version = Version(0);
9277
9278 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9279 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9280 let sketch_id = sketch_object.id;
9281 let sketch = expect_sketch(sketch_object);
9282 let line_end_id = *sketch.segments.get(1).unwrap();
9283
9284 let constraint = Constraint::Fixed(Fixed {
9285 points: vec![FixedPoint {
9286 point: line_end_id,
9287 position: Point2d {
9288 x: Number {
9289 value: 20.0,
9290 units: NumericSuffix::Mm,
9291 },
9292 y: Number {
9293 value: 0.0,
9294 units: NumericSuffix::Mm,
9295 },
9296 },
9297 }],
9298 });
9299 let (constraint_source, _) = frontend
9300 .add_constraint(&mock_ctx, version, sketch_id, constraint)
9301 .await
9302 .unwrap();
9303
9304 assert!(
9305 constraint_source
9306 .text
9307 .contains("line1 = line(start = [var 0, var 0], end = [var 20, var 0])"),
9308 "{}",
9309 constraint_source.text
9310 );
9311 let (mock_source, _) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
9312 assert_eq!(mock_source.text, constraint_source.text);
9313
9314 mock_ctx.close().await;
9315 }
9316
9317 #[test]
9318 fn test_no_solver_feedback_preserves_original_source() {
9319 let initial_source = "\
9320@settings(defaultLengthUnit = in, kclVersion = 2.0)
9321cylinder = startSketchOn(XY)
9322 |> circle(center= [0, 0], radius= 22)
9323 |> extrude(length = 14)
9324";
9325 let mut frontend = FrontendState::new();
9326 frontend.program = Program::parse(initial_source).unwrap().0.unwrap();
9327 let outcome = ExecOutcome {
9328 variables: Default::default(),
9329 test_program_memory: Default::default(),
9330 operations: Default::default(),
9331 artifact_graph: Default::default(),
9332 scene_objects: Default::default(),
9333 source_range_to_object: Default::default(),
9334 var_solutions: Default::default(),
9335 refactor_metadata: Default::default(),
9336 issues: Default::default(),
9337 filenames: Default::default(),
9338 engine_scene_generation: 0,
9339 source_files: Default::default(),
9340 default_planes: Default::default(),
9341 };
9342
9343 let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9344
9345 assert_eq!(source_delta.text, initial_source);
9346 }
9347
9348 #[tokio::test(flavor = "multi_thread")]
9351 async fn test_edit_segments_with_anchor_ids_limits_drag_fixed_constraints() {
9352 let initial_source = "\
9353sketch(on = XY) {
9354 point1 = point(at = [var 0mm, var 0mm])
9355 point2 = point(at = [var 0mm, var 0mm])
9356 coincident([point1, point2])
9357}
9358";
9359
9360 let program = Program::parse(initial_source).unwrap().0.unwrap();
9361 let mut frontend = FrontendState::new();
9362 let mock_ctx = ExecutorContext::new_mock(None).await;
9363 let version = Version(0);
9364
9365 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9366 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9367 let sketch_id = sketch_object.id;
9368 let sketch = expect_sketch(sketch_object);
9369 let point1_id = sketch.segments[0];
9370 let point2_id = sketch.segments[1];
9371
9372 let segments = vec![
9373 ExistingSegmentCtor {
9374 id: point1_id,
9375 ctor: SegmentCtor::Point(PointCtor {
9376 position: point_expr_mm(10.0, 0.0),
9377 }),
9378 },
9379 ExistingSegmentCtor {
9380 id: point2_id,
9381 ctor: SegmentCtor::Point(PointCtor {
9382 position: point_expr_mm(100.0, 0.0),
9383 }),
9384 },
9385 ];
9386 let (_, scene_delta) = frontend
9387 .edit_segments_with_options(
9388 &mock_ctx,
9389 version,
9390 sketch_id,
9391 segments,
9392 EditSegmentsOptions {
9393 anchor_segment_ids: Some(vec![point1_id]),
9394 drag_anchors: Vec::new(),
9395 constraint_label_edits: Vec::new(),
9396 commit_solved_initial_guesses: true,
9397 },
9398 )
9399 .await
9400 .unwrap();
9401
9402 assert_point_position_close(
9403 point_position(&scene_delta.new_graph, point1_id),
9404 point_number_mm(10.0, 0.0),
9405 );
9406 assert_point_position_close(
9407 point_position(&scene_delta.new_graph, point2_id),
9408 point_number_mm(10.0, 0.0),
9409 );
9410
9411 mock_ctx.close().await;
9412 }
9413
9414 fn collect_sketch_var_literals_with_value(program: &Program, value: f64) -> Vec<(SourceRange, ast::NodePath)> {
9417 use std::cell::RefCell;
9418 struct Collector {
9419 target: f64,
9420 out: RefCell<Vec<(SourceRange, ast::NodePath)>>,
9421 }
9422 impl<'a> crate::walk::Visitor<'a> for &Collector {
9423 type Error = crate::front::Error;
9424 fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
9425 if let crate::walk::Node::SketchVar(sketch_var) = node
9426 && let (Some(initial), Some(node_path)) = (&sketch_var.initial, &sketch_var.node_path)
9427 && (initial.value - self.target).abs() < 1e-9
9428 {
9429 self.out
9430 .borrow_mut()
9431 .push((SourceRange::from(initial.as_ref()), node_path.clone()));
9432 }
9433 for child in node.children().iter() {
9434 if !child.visit(*self)? {
9435 return Ok(false);
9436 }
9437 }
9438 Ok(true)
9439 }
9440 }
9441 let collector = Collector {
9442 target: value,
9443 out: Default::default(),
9444 };
9445 let _ = crate::walk::Node::from(&program.ast).visit(&collector);
9446 collector.out.into_inner()
9447 }
9448
9449 fn collect_all_sketch_vars(program: &Program) -> Vec<(SourceRange, ast::NodePath)> {
9452 use std::cell::RefCell;
9453 struct Collector {
9454 out: RefCell<Vec<(SourceRange, ast::NodePath)>>,
9455 }
9456 impl<'a> crate::walk::Visitor<'a> for &Collector {
9457 type Error = crate::front::Error;
9458 fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
9459 if let crate::walk::Node::SketchVar(sketch_var) = node
9460 && let Some(node_path) = &sketch_var.node_path
9461 {
9462 self.out
9463 .borrow_mut()
9464 .push((SourceRange::from(sketch_var), node_path.clone()));
9465 }
9466 for child in node.children().iter() {
9467 if !child.visit(*self)? {
9468 return Ok(false);
9469 }
9470 }
9471 Ok(true)
9472 }
9473 }
9474 let collector = Collector {
9475 out: Default::default(),
9476 };
9477 let _ = crate::walk::Node::from(&program.ast).visit(&collector);
9478 collector.out.into_inner()
9479 }
9480
9481 fn empty_exec_outcome_with_var_solutions(
9482 var_solutions: Vec<(SourceRange, Option<ast::NodePath>, Number)>,
9483 ) -> ExecOutcome {
9484 ExecOutcome {
9485 variables: Default::default(),
9486 test_program_memory: Default::default(),
9487 operations: Default::default(),
9488 artifact_graph: Default::default(),
9489 scene_objects: Default::default(),
9490 source_range_to_object: Default::default(),
9491 var_solutions,
9492 refactor_metadata: Default::default(),
9493 issues: Default::default(),
9494 filenames: Default::default(),
9495 engine_scene_generation: 0,
9496 source_files: Default::default(),
9497 default_planes: Default::default(),
9498 }
9499 }
9500
9501 #[test]
9505 fn test_commit_var_solution_by_node_path_updates_sketch_var() {
9506 let initial_source = "\
9507sketch(on = XY) {
9508 line1 = line(start = [var 0, var 0], end = [var 10mm, var 0])
9509}
9510";
9511 let program = Program::parse(initial_source).unwrap().0.unwrap();
9512 let matches = collect_sketch_var_literals_with_value(&program, 10.0);
9513 assert_eq!(matches.len(), 1, "expected exactly one `var 10mm`");
9514 let (literal_range, node_path) = matches.into_iter().next().unwrap();
9515
9516 let mut frontend = FrontendState::new();
9517 frontend.program = program;
9518
9519 let outcome = empty_exec_outcome_with_var_solutions(vec![(
9520 literal_range,
9521 Some(node_path),
9522 Number {
9523 value: 25.0,
9524 units: NumericSuffix::Mm,
9525 },
9526 )]);
9527
9528 let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9529
9530 insta::assert_snapshot!(
9531 "test_commit_var_solution_by_node_path_updates_sketch_var",
9532 source_delta.text
9533 );
9534 }
9535
9536 #[test]
9543 fn test_commit_var_solution_survives_whitespace_shift_earlier_in_file() {
9544 let compact_source = "\
9545sketch(on = XY) {
9546 line1 = line(start = [var 0, var 0], end = [var 10mm, var 0])
9547}
9548";
9549 let padded_source = "\
9550// added comment\n// added comment\n\nsketch(on = XY) {
9551 line1 = line(start = [var 0, var 0], end = [var 10mm, var 0])
9552}
9553";
9554 let compact_program = Program::parse(compact_source).unwrap().0.unwrap();
9555 let padded_program = Program::parse(padded_source).unwrap().0.unwrap();
9556
9557 let compact_match = collect_sketch_var_literals_with_value(&compact_program, 10.0)
9558 .into_iter()
9559 .next()
9560 .expect("expected `var 10mm` in compact source");
9561 let padded_match = collect_sketch_var_literals_with_value(&padded_program, 10.0)
9562 .into_iter()
9563 .next()
9564 .expect("expected `var 10mm` in padded source");
9565
9566 assert_ne!(
9567 compact_match.0, padded_match.0,
9568 "byte offsets must differ for this test to be meaningful"
9569 );
9570 assert_eq!(
9571 compact_match.1, padded_match.1,
9572 "node paths must agree across whitespace; that's the whole point of NodePath",
9573 );
9574
9575 let mut frontend = FrontendState::new();
9576 frontend.program = padded_program;
9577
9578 let outcome = empty_exec_outcome_with_var_solutions(vec![(
9580 compact_match.0,
9581 Some(compact_match.1),
9582 Number {
9583 value: 30.0,
9584 units: NumericSuffix::Mm,
9585 },
9586 )]);
9587
9588 let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9589
9590 insta::assert_snapshot!(
9591 "test_commit_var_solution_survives_whitespace_shift_earlier_in_file",
9592 source_delta.text
9593 );
9594 }
9595
9596 #[test]
9600 fn test_commit_var_solution_node_path_wins_when_source_range_points_at_wrong_var() {
9601 let initial_source = "\
9602sketch(on = XY) {
9603 line1 = line(start = [var 10mm, var 0mm], end = [var 20mm, var 0mm])
9604}
9605";
9606 let program = Program::parse(initial_source).unwrap().0.unwrap();
9607
9608 let var_10 = collect_sketch_var_literals_with_value(&program, 10.0)
9609 .into_iter()
9610 .next()
9611 .expect("expected `var 10mm`");
9612 let var_20 = collect_sketch_var_literals_with_value(&program, 20.0)
9613 .into_iter()
9614 .next()
9615 .expect("expected `var 20mm`");
9616
9617 let mut frontend = FrontendState::new();
9618 frontend.program = program;
9619
9620 let outcome = empty_exec_outcome_with_var_solutions(vec![(
9622 var_20.0,
9623 Some(var_10.1),
9624 Number {
9625 value: 33.0,
9626 units: NumericSuffix::Mm,
9627 },
9628 )]);
9629
9630 let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9631
9632 insta::assert_snapshot!(
9633 "test_commit_var_solution_node_path_wins_when_source_range_points_at_wrong_var",
9634 source_delta.text
9635 );
9636 }
9637
9638 #[test]
9645 fn test_commit_var_solution_writes_back_into_bare_var() {
9646 let initial_source = "\
9647@settings(experimentalFeatures = allow, kclVersion = 2.0)
9648sketch(on = XY) {
9649 line1 = line(start = [var, var 0mm], end = [var 10mm, var 0])
9650}
9651";
9652 let program = Program::parse(initial_source).unwrap().0.unwrap();
9653
9654 let bare = collect_all_sketch_vars(&program)
9657 .into_iter()
9658 .find(|(range, _)| {
9659 range.end() - range.start() == 3
9661 })
9662 .expect("expected at least one bare `var`");
9663
9664 let mut frontend = FrontendState::new();
9665 frontend.program = program;
9666
9667 let outcome = empty_exec_outcome_with_var_solutions(vec![(
9668 bare.0,
9669 Some(bare.1),
9670 Number {
9671 value: 7.0,
9672 units: NumericSuffix::Mm,
9673 },
9674 )]);
9675
9676 let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9677
9678 insta::assert_snapshot!("test_commit_var_solution_writes_back_into_bare_var", source_delta.text);
9682 }
9683
9684 #[tokio::test(flavor = "multi_thread")]
9685 async fn test_delete_point_without_var() {
9686 let initial_source = "\
9687sketch(on = XY) {
9688 point(at = [var 1, var 2])
9689 point(at = [var 3, var 4])
9690 point(at = [var 5, var 6])
9691}
9692";
9693
9694 let program = Program::parse(initial_source).unwrap().0.unwrap();
9695
9696 let mut frontend = FrontendState::new();
9697
9698 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
9699 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
9700 let version = Version(0);
9701
9702 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9703 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9704 let sketch_id = sketch_object.id;
9705 let sketch = expect_sketch(sketch_object);
9706
9707 let point_id = *sketch.segments.get(1).unwrap();
9708
9709 let (src_delta, scene_delta) = frontend
9710 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
9711 .await
9712 .unwrap();
9713 insta::assert_snapshot!("test_delete_point_without_var", src_delta.text.as_str());
9714 assert_eq!(scene_delta.new_objects, vec![]);
9715 assert_eq!(scene_delta.new_graph.objects.len(), 4);
9716
9717 ctx.close().await;
9718 mock_ctx.close().await;
9719 }
9720
9721 #[tokio::test(flavor = "multi_thread")]
9722 async fn test_delete_point_with_var() {
9723 let initial_source = "\
9724sketch(on = XY) {
9725 point(at = [var 1, var 2])
9726 point1 = point(at = [var 3, var 4])
9727 point(at = [var 5, var 6])
9728}
9729";
9730
9731 let program = Program::parse(initial_source).unwrap().0.unwrap();
9732
9733 let mut frontend = FrontendState::new();
9734
9735 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
9736 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
9737 let version = Version(0);
9738
9739 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9740 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9741 let sketch_id = sketch_object.id;
9742 let sketch = expect_sketch(sketch_object);
9743
9744 let point_id = *sketch.segments.get(1).unwrap();
9745
9746 let (src_delta, scene_delta) = frontend
9747 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
9748 .await
9749 .unwrap();
9750 insta::assert_snapshot!("test_delete_point_with_var", src_delta.text.as_str());
9751 assert_eq!(scene_delta.new_objects, vec![]);
9752 assert_eq!(scene_delta.new_graph.objects.len(), 4);
9753
9754 ctx.close().await;
9755 mock_ctx.close().await;
9756 }
9757
9758 #[tokio::test(flavor = "multi_thread")]
9759 async fn test_delete_multiple_points() {
9760 let initial_source = "\
9761sketch(on = XY) {
9762 point(at = [var 1, var 2])
9763 point1 = point(at = [var 3, var 4])
9764 point(at = [var 5, var 6])
9765}
9766";
9767
9768 let program = Program::parse(initial_source).unwrap().0.unwrap();
9769
9770 let mut frontend = FrontendState::new();
9771
9772 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
9773 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
9774 let version = Version(0);
9775
9776 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9777 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9778 let sketch_id = sketch_object.id;
9779
9780 let sketch = expect_sketch(sketch_object);
9781
9782 let point1_id = *sketch.segments.first().unwrap();
9783 let point2_id = *sketch.segments.get(1).unwrap();
9784
9785 let (src_delta, scene_delta) = frontend
9786 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point1_id, point2_id])
9787 .await
9788 .unwrap();
9789 insta::assert_snapshot!("test_delete_multiple_points", src_delta.text.as_str());
9790 assert_eq!(scene_delta.new_objects, vec![]);
9791 assert_eq!(scene_delta.new_graph.objects.len(), 3);
9792
9793 ctx.close().await;
9794 mock_ctx.close().await;
9795 }
9796
9797 #[tokio::test(flavor = "multi_thread")]
9798 async fn test_delete_coincident_constraint() {
9799 let initial_source = "\
9800sketch(on = XY) {
9801 point1 = point(at = [var 1, var 2])
9802 point2 = point(at = [var 3, var 4])
9803 coincident([point1, point2])
9804 point(at = [var 5, var 6])
9805}
9806";
9807
9808 let program = Program::parse(initial_source).unwrap().0.unwrap();
9809
9810 let mut frontend = FrontendState::new();
9811
9812 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
9813 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
9814 let version = Version(0);
9815
9816 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9817 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9818 let sketch_id = sketch_object.id;
9819 let sketch = expect_sketch(sketch_object);
9820
9821 let coincident_id = *sketch.constraints.first().unwrap();
9822
9823 let (src_delta, scene_delta) = frontend
9824 .delete_objects(&mock_ctx, version, sketch_id, vec![coincident_id], Vec::new())
9825 .await
9826 .unwrap();
9827 insta::assert_snapshot!("test_delete_coincident_constraint", src_delta.text.as_str());
9828 assert_eq!(scene_delta.new_objects, vec![]);
9829 assert_eq!(scene_delta.new_graph.objects.len(), 5);
9830
9831 ctx.close().await;
9832 mock_ctx.close().await;
9833 }
9834
9835 #[tokio::test(flavor = "multi_thread")]
9836 async fn test_delete_line_cascades_to_coincident_constraint() {
9837 let initial_source = "\
9838sketch(on = XY) {
9839 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9840 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9841 coincident([line1.end, line2.start])
9842}
9843";
9844
9845 let program = Program::parse(initial_source).unwrap().0.unwrap();
9846
9847 let mut frontend = FrontendState::new();
9848
9849 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
9850 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
9851 let version = Version(0);
9852
9853 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9854 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9855 let sketch_id = sketch_object.id;
9856 let sketch = expect_sketch(sketch_object);
9857 let line_id = *sketch.segments.get(5).unwrap();
9858
9859 let (src_delta, scene_delta) = frontend
9860 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
9861 .await
9862 .unwrap();
9863 insta::assert_snapshot!(
9864 "test_delete_line_cascades_to_coincident_constraint",
9865 src_delta.text.as_str()
9866 );
9867 assert_eq!(
9868 scene_delta.new_graph.objects.len(),
9869 5,
9870 "{:#?}",
9871 scene_delta.new_graph.objects
9872 );
9873
9874 ctx.close().await;
9875 mock_ctx.close().await;
9876 }
9877
9878 #[tokio::test(flavor = "multi_thread")]
9879 async fn test_delete_line_cascades_to_distance_constraint() {
9880 let initial_source = "\
9881sketch(on = XY) {
9882 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9883 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9884 distance([line1.end, line2.start]) == 10mm
9885}
9886";
9887
9888 let program = Program::parse(initial_source).unwrap().0.unwrap();
9889
9890 let mut frontend = FrontendState::new();
9891
9892 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
9893 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
9894 let version = Version(0);
9895
9896 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9897 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9898 let sketch_id = sketch_object.id;
9899 let sketch = expect_sketch(sketch_object);
9900 let line_id = *sketch.segments.get(5).unwrap();
9901
9902 let (src_delta, scene_delta) = frontend
9903 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
9904 .await
9905 .unwrap();
9906 insta::assert_snapshot!(
9907 "test_delete_line_cascades_to_distance_constraint",
9908 src_delta.text.as_str()
9909 );
9910 assert_eq!(
9911 scene_delta.new_graph.objects.len(),
9912 5,
9913 "{:#?}",
9914 scene_delta.new_graph.objects
9915 );
9916
9917 ctx.close().await;
9918 mock_ctx.close().await;
9919 }
9920
9921 #[tokio::test(flavor = "multi_thread")]
9922 async fn test_delete_point_cascades_to_horizontal_distance_constraint() {
9923 let initial_source = "\
9924sketch(on = XY) {
9925 point1 = point(at = [var 1, var 2])
9926 point2 = point(at = [var 3, var 4])
9927 horizontalDistance([point1, point2]) == 10mm
9928}
9929";
9930
9931 let program = Program::parse(initial_source).unwrap().0.unwrap();
9932
9933 let mut frontend = FrontendState::new();
9934
9935 let mock_ctx = ExecutorContext::new_mock(None).await;
9936 let version = Version(0);
9937
9938 frontend.program = program.clone();
9939 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9940 frontend.update_state_after_exec(outcome, true);
9941 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9942 let sketch_id = sketch_object.id;
9943 let sketch = expect_sketch(sketch_object);
9944 let point2_id = *sketch.segments.get(1).unwrap();
9945
9946 let (src_delta, scene_delta) = frontend
9947 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point2_id])
9948 .await
9949 .unwrap();
9950 insta::assert_snapshot!(
9951 "test_delete_point_cascades_to_horizontal_distance_constraint",
9952 src_delta.text.as_str()
9953 );
9954 assert_eq!(
9955 scene_delta.new_graph.objects.len(),
9956 3,
9957 "{:#?}",
9958 scene_delta.new_graph.objects
9959 );
9960
9961 mock_ctx.close().await;
9962 }
9963
9964 #[tokio::test(flavor = "multi_thread")]
9965 async fn test_delete_line_cascades_to_fixed_constraint() {
9966 let initial_source = "\
9967sketch(on = XY) {
9968 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9969 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9970 fixed([line1.start, [0, 0]])
9971}
9972";
9973
9974 let program = Program::parse(initial_source).unwrap().0.unwrap();
9975
9976 let mut frontend = FrontendState::new();
9977
9978 let mock_ctx = ExecutorContext::new_mock(None).await;
9979 let version = Version(0);
9980
9981 frontend.program = program.clone();
9982 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9983 frontend.update_state_after_exec(outcome, true);
9984 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9985 let sketch_id = sketch_object.id;
9986 let sketch = expect_sketch(sketch_object);
9987 let line1_id = *sketch.segments.get(2).unwrap();
9988
9989 let (src_delta, scene_delta) = frontend
9990 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
9991 .await
9992 .unwrap();
9993 insta::assert_snapshot!("test_delete_line_cascades_to_fixed_constraint", src_delta.text.as_str());
9994 assert_eq!(
9995 scene_delta.new_graph.objects.len(),
9996 5,
9997 "{:#?}",
9998 scene_delta.new_graph.objects
9999 );
10000
10001 mock_ctx.close().await;
10002 }
10003
10004 #[tokio::test(flavor = "multi_thread")]
10005 async fn test_delete_line_cascades_to_midpoint_constraint() {
10006 let initial_source = "\
10007sketch(on = XY) {
10008 point1 = point(at = [var 1, var 2])
10009 line1 = line(start = [var 0, var 0], end = [var 6, var 4])
10010 midpoint(line1, point = point1)
10011}
10012";
10013
10014 let program = Program::parse(initial_source).unwrap().0.unwrap();
10015
10016 let mut frontend = FrontendState::new();
10017
10018 let mock_ctx = ExecutorContext::new_mock(None).await;
10019 let version = Version(0);
10020
10021 frontend.program = program.clone();
10022 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10023 frontend.update_state_after_exec(outcome, true);
10024 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10025 let sketch_id = sketch_object.id;
10026 let sketch = expect_sketch(sketch_object);
10027 let line1_id = *sketch.segments.get(3).unwrap();
10028
10029 let (src_delta, scene_delta) = frontend
10030 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
10031 .await
10032 .unwrap();
10033 insta::assert_snapshot!(
10034 "test_delete_line_cascades_to_midpoint_constraint",
10035 src_delta.text.as_str()
10036 );
10037 assert_eq!(
10038 scene_delta.new_graph.objects.len(),
10039 3,
10040 "{:#?}",
10041 scene_delta.new_graph.objects
10042 );
10043
10044 mock_ctx.close().await;
10045 }
10046
10047 #[tokio::test(flavor = "multi_thread")]
10048 async fn test_delete_point_preserves_multiline_coincident_constraint() {
10049 let initial_source = "\
10050sketch(on = XY) {
10051 point1 = point(at = [var 1, var 2])
10052 point2 = point(at = [var 3, var 4])
10053 point3 = point(at = [var 5, var 6])
10054 coincident([point1, point2, point3])
10055}
10056";
10057
10058 let program = Program::parse(initial_source).unwrap().0.unwrap();
10059
10060 let mut frontend = FrontendState::new();
10061
10062 let mock_ctx = ExecutorContext::new_mock(None).await;
10063 let version = Version(0);
10064
10065 frontend.program = program.clone();
10066 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10067 frontend.update_state_after_exec(outcome, true);
10068 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10069 let sketch_id = sketch_object.id;
10070 let sketch = expect_sketch(sketch_object);
10071 let point3_id = *sketch.segments.get(2).unwrap();
10072
10073 let (src_delta, scene_delta) = frontend
10074 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point3_id])
10075 .await
10076 .unwrap();
10077 assert!(src_delta.text.contains("point1 = point("), "{}", src_delta.text);
10078 assert!(src_delta.text.contains("point2 = point("), "{}", src_delta.text);
10079 assert!(!src_delta.text.contains("point3 = point("), "{}", src_delta.text);
10080 assert!(
10081 src_delta.text.contains("coincident([point1, point2])"),
10082 "{}",
10083 src_delta.text
10084 );
10085
10086 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10087 let sketch = expect_sketch(sketch_object);
10088 assert_eq!(sketch.segments.len(), 2);
10089 assert_eq!(sketch.constraints.len(), 1);
10090
10091 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10092 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10093 panic!("Expected constraint object");
10094 };
10095 let Constraint::Coincident(coincident) = constraint else {
10096 panic!("Expected coincident constraint");
10097 };
10098 assert_eq!(
10099 coincident.segments,
10100 sketch
10101 .segments
10102 .iter()
10103 .copied()
10104 .map(Into::into)
10105 .collect::<Vec<ConstraintSegment>>()
10106 );
10107
10108 mock_ctx.close().await;
10109 }
10110
10111 #[tokio::test(flavor = "multi_thread")]
10112 async fn test_delete_line_preserves_multiline_equal_length_constraint() {
10113 let initial_source = "\
10114sketch(on = XY) {
10115 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10116 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10117 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10118 equalLength([line1, line2, line3])
10119}
10120";
10121
10122 let program = Program::parse(initial_source).unwrap().0.unwrap();
10123
10124 let mut frontend = FrontendState::new();
10125
10126 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
10127 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
10128 let version = Version(0);
10129
10130 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10131 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10132 let sketch_id = sketch_object.id;
10133 let sketch = expect_sketch(sketch_object);
10134 let line3_id = *sketch.segments.get(8).unwrap();
10135
10136 let (src_delta, scene_delta) = frontend
10137 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line3_id])
10138 .await
10139 .unwrap();
10140 insta::assert_snapshot!(
10141 "test_delete_line_preserves_multiline_equal_length_constraint",
10142 src_delta.text.as_str()
10143 );
10144
10145 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10146 let sketch = expect_sketch(sketch_object);
10147 assert_eq!(sketch.constraints.len(), 1);
10148
10149 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10150 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10151 panic!("Expected constraint object");
10152 };
10153 let Constraint::LinesEqualLength(lines_equal_length) = constraint else {
10154 panic!("Expected lines equal length constraint");
10155 };
10156 assert_eq!(lines_equal_length.lines.len(), 2);
10157
10158 ctx.close().await;
10159 mock_ctx.close().await;
10160 }
10161
10162 #[tokio::test(flavor = "multi_thread")]
10163 async fn test_delete_line_preserves_multiline_horizontal_constraint() {
10164 let initial_source = "\
10165sketch(on = XY) {
10166 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10167 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10168 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10169 horizontal([line1.end, line2.start, line3.start])
10170}
10171";
10172
10173 let program = Program::parse(initial_source).unwrap().0.unwrap();
10174
10175 let mut frontend = FrontendState::new();
10176
10177 let mock_ctx = ExecutorContext::new_mock(None).await;
10178 let version = Version(0);
10179
10180 frontend.program = program.clone();
10181 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10182 frontend.update_state_after_exec(outcome, true);
10183 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10184 let sketch_id = sketch_object.id;
10185 let sketch = expect_sketch(sketch_object);
10186 let line1_id = *sketch.segments.get(2).unwrap();
10187
10188 let (src_delta, scene_delta) = frontend
10189 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
10190 .await
10191 .unwrap();
10192 assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
10193 assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
10194 assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
10195 assert!(
10196 src_delta.text.contains("horizontal([line2.start, line3.start])"),
10197 "{}",
10198 src_delta.text
10199 );
10200
10201 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10202 let sketch = expect_sketch(sketch_object);
10203 assert_eq!(sketch.constraints.len(), 1);
10204
10205 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10206 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10207 panic!("Expected constraint object");
10208 };
10209 let Constraint::Horizontal(Horizontal::Points { points }) = constraint else {
10210 panic!("Expected horizontal points constraint");
10211 };
10212 let remaining_points = vec![sketch.segments[0].into(), sketch.segments[3].into()];
10213 assert_eq!(*points, remaining_points);
10214
10215 mock_ctx.close().await;
10216 }
10217
10218 #[tokio::test(flavor = "multi_thread")]
10219 async fn test_delete_line_preserves_multiline_vertical_constraint() {
10220 let initial_source = "\
10221sketch(on = XY) {
10222 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10223 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10224 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10225 vertical([line1.end, line2.start, line3.start])
10226}
10227";
10228
10229 let program = Program::parse(initial_source).unwrap().0.unwrap();
10230
10231 let mut frontend = FrontendState::new();
10232
10233 let mock_ctx = ExecutorContext::new_mock(None).await;
10234 let version = Version(0);
10235
10236 frontend.program = program.clone();
10237 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10238 frontend.update_state_after_exec(outcome, true);
10239 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10240 let sketch_id = sketch_object.id;
10241 let sketch = expect_sketch(sketch_object);
10242 let line1_id = *sketch.segments.get(2).unwrap();
10243
10244 let (src_delta, scene_delta) = frontend
10245 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
10246 .await
10247 .unwrap();
10248 assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
10249 assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
10250 assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
10251 assert!(
10252 src_delta.text.contains("vertical([line2.start, line3.start])"),
10253 "{}",
10254 src_delta.text
10255 );
10256
10257 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10258 let sketch = expect_sketch(sketch_object);
10259 assert_eq!(sketch.constraints.len(), 1);
10260
10261 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10262 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10263 panic!("Expected constraint object");
10264 };
10265 let Constraint::Vertical(Vertical::Points { points }) = constraint else {
10266 panic!("Expected vertical points constraint");
10267 };
10268 let remaining_points = vec![sketch.segments[0].into(), sketch.segments[3].into()];
10269 assert_eq!(*points, remaining_points);
10270
10271 mock_ctx.close().await;
10272 }
10273
10274 #[tokio::test(flavor = "multi_thread")]
10275 async fn test_delete_line_preserves_multiline_coincident_constraint() {
10276 let initial_source = "\
10277sketch(on = XY) {
10278 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10279 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10280 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10281 coincident([line1.end, line2.start, line3.start])
10282}
10283";
10284
10285 let program = Program::parse(initial_source).unwrap().0.unwrap();
10286
10287 let mut frontend = FrontendState::new();
10288
10289 let mock_ctx = ExecutorContext::new_mock(None).await;
10290 let version = Version(0);
10291
10292 frontend.program = program.clone();
10293 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10294 frontend.update_state_after_exec(outcome, true);
10295 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10296 let sketch_id = sketch_object.id;
10297 let sketch = expect_sketch(sketch_object);
10298 let line1_id = *sketch.segments.get(2).unwrap();
10299
10300 let (src_delta, scene_delta) = frontend
10301 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
10302 .await
10303 .unwrap();
10304 assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
10305 assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
10306 assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
10307 assert!(
10308 src_delta.text.contains("coincident([line2.start, line3.start])"),
10309 "{}",
10310 src_delta.text
10311 );
10312
10313 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10314 let sketch = expect_sketch(sketch_object);
10315 assert_eq!(sketch.constraints.len(), 1);
10316
10317 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10318 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10319 panic!("Expected constraint object");
10320 };
10321 let Constraint::Coincident(coincident) = constraint else {
10322 panic!("Expected coincident constraint");
10323 };
10324 let remaining_segments = vec![sketch.segments[0].into(), sketch.segments[3].into()];
10325 assert_eq!(coincident.segments, remaining_segments);
10326
10327 mock_ctx.close().await;
10328 }
10329
10330 #[tokio::test(flavor = "multi_thread")]
10331 async fn test_delete_lines_removes_multiline_equal_length_constraint_below_minimum() {
10332 let initial_source = "\
10333sketch(on = XY) {
10334 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10335 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10336 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10337 equalLength([line1, line2, line3])
10338}
10339";
10340
10341 let program = Program::parse(initial_source).unwrap().0.unwrap();
10342
10343 let mut frontend = FrontendState::new();
10344
10345 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
10346 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
10347 let version = Version(0);
10348
10349 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10350 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10351 let sketch_id = sketch_object.id;
10352 let sketch = expect_sketch(sketch_object);
10353 let line2_id = *sketch.segments.get(5).unwrap();
10354 let line3_id = *sketch.segments.get(8).unwrap();
10355
10356 let (src_delta, scene_delta) = frontend
10357 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line2_id, line3_id])
10358 .await
10359 .unwrap();
10360 insta::assert_snapshot!(
10361 "test_delete_lines_removes_multiline_equal_length_constraint_below_minimum",
10362 src_delta.text.as_str()
10363 );
10364
10365 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10366 let sketch = expect_sketch(sketch_object);
10367 assert!(sketch.constraints.is_empty());
10368
10369 ctx.close().await;
10370 mock_ctx.close().await;
10371 }
10372
10373 #[tokio::test(flavor = "multi_thread")]
10374 async fn test_delete_line_preserves_multiline_parallel_constraint() {
10375 let initial_source = "\
10376sketch(on = XY) {
10377 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10378 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10379 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10380 parallel([line1, line2, line3])
10381}
10382";
10383
10384 let program = Program::parse(initial_source).unwrap().0.unwrap();
10385
10386 let mut frontend = FrontendState::new();
10387
10388 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
10389 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
10390 let version = Version(0);
10391
10392 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10393 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10394 let sketch_id = sketch_object.id;
10395 let sketch = expect_sketch(sketch_object);
10396 let line3_id = *sketch.segments.get(8).unwrap();
10397
10398 let (src_delta, scene_delta) = frontend
10399 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line3_id])
10400 .await
10401 .unwrap();
10402 insta::assert_snapshot!(
10403 "test_delete_line_preserves_multiline_parallel_constraint",
10404 src_delta.text.as_str()
10405 );
10406
10407 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10408 let sketch = expect_sketch(sketch_object);
10409 assert_eq!(sketch.constraints.len(), 1);
10410
10411 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10412 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10413 panic!("Expected constraint object");
10414 };
10415 let Constraint::Parallel(parallel) = constraint else {
10416 panic!("Expected parallel constraint");
10417 };
10418 assert_eq!(parallel.lines.len(), 2);
10419
10420 ctx.close().await;
10421 mock_ctx.close().await;
10422 }
10423
10424 #[tokio::test(flavor = "multi_thread")]
10425 async fn test_delete_lines_removes_multiline_parallel_constraint_below_minimum() {
10426 let initial_source = "\
10427sketch(on = XY) {
10428 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10429 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10430 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10431 parallel([line1, line2, line3])
10432}
10433";
10434
10435 let program = Program::parse(initial_source).unwrap().0.unwrap();
10436
10437 let mut frontend = FrontendState::new();
10438
10439 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
10440 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
10441 let version = Version(0);
10442
10443 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10444 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10445 let sketch_id = sketch_object.id;
10446 let sketch = expect_sketch(sketch_object);
10447 let line2_id = *sketch.segments.get(5).unwrap();
10448 let line3_id = *sketch.segments.get(8).unwrap();
10449
10450 let (src_delta, scene_delta) = frontend
10451 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line2_id, line3_id])
10452 .await
10453 .unwrap();
10454 insta::assert_snapshot!(
10455 "test_delete_lines_removes_multiline_parallel_constraint_below_minimum",
10456 src_delta.text.as_str()
10457 );
10458
10459 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10460 let sketch = expect_sketch(sketch_object);
10461 assert!(sketch.constraints.is_empty());
10462
10463 ctx.close().await;
10464 mock_ctx.close().await;
10465 }
10466
10467 #[tokio::test(flavor = "multi_thread")]
10468 async fn test_delete_line_line_coincident_constraint() {
10469 let initial_source = "\
10470sketch(on = XY) {
10471 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10472 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10473 coincident([line1, line2])
10474}
10475";
10476
10477 let program = Program::parse(initial_source).unwrap().0.unwrap();
10478
10479 let mut frontend = FrontendState::new();
10480
10481 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
10482 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
10483 let version = Version(0);
10484
10485 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10486 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10487 let sketch_id = sketch_object.id;
10488 let sketch = expect_sketch(sketch_object);
10489
10490 let coincident_id = *sketch.constraints.first().unwrap();
10491
10492 let (src_delta, scene_delta) = frontend
10493 .delete_objects(&mock_ctx, version, sketch_id, vec![coincident_id], Vec::new())
10494 .await
10495 .unwrap();
10496 insta::assert_snapshot!("test_delete_line_line_coincident_constraint", src_delta.text.as_str());
10497 assert_eq!(scene_delta.new_objects, vec![]);
10498 assert_eq!(scene_delta.new_graph.objects.len(), 8);
10499
10500 ctx.close().await;
10501 mock_ctx.close().await;
10502 }
10503
10504 #[tokio::test(flavor = "multi_thread")]
10505 async fn test_two_points_coincident() {
10506 let initial_source = "\
10507sketch(on = XY) {
10508 point1 = point(at = [var 1, var 2])
10509 point(at = [3, 4])
10510}
10511";
10512
10513 let program = Program::parse(initial_source).unwrap().0.unwrap();
10514
10515 let mut frontend = FrontendState::new();
10516
10517 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
10518 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
10519 let version = Version(0);
10520
10521 frontend.hack_set_program(&ctx, program).await.unwrap();
10522 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10523 let sketch_id = sketch_object.id;
10524 let sketch = expect_sketch(sketch_object);
10525 let point0_id = *sketch.segments.first().unwrap();
10526 let point1_id = *sketch.segments.get(1).unwrap();
10527
10528 let constraint = Constraint::Coincident(Coincident {
10529 segments: vec![point0_id.into(), point1_id.into()],
10530 });
10531 let (src_delta, scene_delta) = frontend
10532 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10533 .await
10534 .unwrap();
10535 insta::assert_snapshot!("test_two_points_coincident", src_delta.text.as_str());
10536 assert_eq!(
10537 scene_delta.new_graph.objects.len(),
10538 5,
10539 "{:#?}",
10540 scene_delta.new_graph.objects
10541 );
10542
10543 ctx.close().await;
10544 mock_ctx.close().await;
10545 }
10546
10547 #[tokio::test(flavor = "multi_thread")]
10548 async fn test_three_points_coincident() {
10549 let initial_source = "\
10550sketch(on = XY) {
10551 point1 = point(at = [var 1, var 2])
10552 point(at = [var 3, var 4])
10553 point(at = [var 5, var 6])
10554}
10555";
10556
10557 let program = Program::parse(initial_source).unwrap().0.unwrap();
10558
10559 let mut frontend = FrontendState::new();
10560
10561 let mock_ctx = ExecutorContext::new_mock(None).await;
10562 let version = Version(0);
10563
10564 frontend.program = program.clone();
10565 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10566 frontend.update_state_after_exec(outcome, true);
10567 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10568 let sketch_id = sketch_object.id;
10569 let sketch = expect_sketch(sketch_object);
10570 let segments = sketch
10571 .segments
10572 .iter()
10573 .take(3)
10574 .copied()
10575 .map(Into::into)
10576 .collect::<Vec<ConstraintSegment>>();
10577
10578 let constraint = Constraint::Coincident(Coincident {
10579 segments: segments.clone(),
10580 });
10581 let (src_delta, scene_delta) = frontend
10582 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10583 .await
10584 .unwrap();
10585 insta::assert_snapshot!("test_three_points_coincident", src_delta.text.as_str());
10586
10587 let constraint_object = scene_delta
10588 .new_graph
10589 .objects
10590 .iter()
10591 .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
10592 .unwrap();
10593
10594 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10595 panic!("expected a constraint object");
10596 };
10597
10598 assert_eq!(constraint, &Constraint::Coincident(Coincident { segments }));
10599
10600 mock_ctx.close().await;
10601 }
10602
10603 #[tokio::test(flavor = "multi_thread")]
10604 async fn test_source_with_three_point_coincident_tracks_all_segments() {
10605 let initial_source = "\
10606sketch(on = XY) {
10607 point1 = point(at = [var 1, var 2])
10608 point2 = point(at = [var 3, var 4])
10609 point3 = point(at = [var 5, var 6])
10610 coincident([point1, point2, point3])
10611}
10612";
10613
10614 let program = Program::parse(initial_source).unwrap().0.unwrap();
10615
10616 let mut frontend = FrontendState::new();
10617
10618 let ctx = ExecutorContext::new_mock(None).await;
10619 frontend.program = program.clone();
10620 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10621 frontend.update_state_after_exec(outcome, true);
10622
10623 let constraint_object = frontend
10624 .scene_graph
10625 .objects
10626 .iter()
10627 .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
10628 .unwrap();
10629 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10630 panic!("expected a constraint object");
10631 };
10632
10633 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10634 let sketch = expect_sketch(sketch_object);
10635 let expected_segments = sketch
10636 .segments
10637 .iter()
10638 .take(3)
10639 .copied()
10640 .map(Into::into)
10641 .collect::<Vec<ConstraintSegment>>();
10642
10643 assert_eq!(
10644 constraint,
10645 &Constraint::Coincident(Coincident {
10646 segments: expected_segments,
10647 })
10648 );
10649
10650 ctx.close().await;
10651 }
10652
10653 #[tokio::test(flavor = "multi_thread")]
10654 async fn test_point_origin_coincident_preserves_order() {
10655 let initial_source = "\
10656sketch(on = XY) {
10657 point(at = [var 1, var 2])
10658}
10659";
10660
10661 for (origin_first, snapshot_name) in [
10662 (true, "test_point_origin_coincident_preserves_order_origin_first"),
10663 (false, "test_point_origin_coincident_preserves_order_point_first"),
10664 ] {
10665 let program = Program::parse(initial_source).unwrap().0.unwrap();
10666
10667 let mut frontend = FrontendState::new();
10668
10669 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
10670 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
10671 let version = Version(0);
10672
10673 frontend.hack_set_program(&ctx, program).await.unwrap();
10674 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10675 let sketch_id = sketch_object.id;
10676 let sketch = expect_sketch(sketch_object);
10677 let point_id = *sketch.segments.first().unwrap();
10678
10679 let segments = if origin_first {
10680 vec![ConstraintSegment::ORIGIN, point_id.into()]
10681 } else {
10682 vec![point_id.into(), ConstraintSegment::ORIGIN]
10683 };
10684 let constraint = Constraint::Coincident(Coincident {
10685 segments: segments.clone(),
10686 });
10687 let (src_delta, scene_delta) = frontend
10688 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10689 .await
10690 .unwrap();
10691 insta::assert_snapshot!(snapshot_name, src_delta.text.as_str());
10692
10693 let constraint_object = scene_delta
10694 .new_graph
10695 .objects
10696 .iter()
10697 .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
10698 .unwrap();
10699
10700 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10701 panic!("expected a constraint object");
10702 };
10703
10704 assert_eq!(constraint, &Constraint::Coincident(Coincident { segments }));
10705
10706 ctx.close().await;
10707 mock_ctx.close().await;
10708 }
10709 }
10710
10711 #[tokio::test(flavor = "multi_thread")]
10712 async fn test_coincident_of_line_end_points() {
10713 let initial_source = "\
10714sketch(on = XY) {
10715 line(start = [var 1, var 2], end = [var 3, var 4])
10716 line(start = [var 5, var 6], end = [var 7, var 8])
10717}
10718";
10719
10720 let program = Program::parse(initial_source).unwrap().0.unwrap();
10721
10722 let mut frontend = FrontendState::new();
10723
10724 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
10725 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
10726 let version = Version(0);
10727
10728 frontend.hack_set_program(&ctx, program).await.unwrap();
10729 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10730 let sketch_id = sketch_object.id;
10731 let sketch = expect_sketch(sketch_object);
10732 let point0_id = *sketch.segments.get(1).unwrap();
10733 let point1_id = *sketch.segments.get(3).unwrap();
10734
10735 let constraint = Constraint::Coincident(Coincident {
10736 segments: vec![point0_id.into(), point1_id.into()],
10737 });
10738 let (src_delta, scene_delta) = frontend
10739 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10740 .await
10741 .unwrap();
10742 insta::assert_snapshot!("test_coincident_of_line_end_points", src_delta.text.as_str());
10743 assert_eq!(
10744 scene_delta.new_graph.objects.len(),
10745 9,
10746 "{:#?}",
10747 scene_delta.new_graph.objects
10748 );
10749
10750 ctx.close().await;
10751 mock_ctx.close().await;
10752 }
10753
10754 #[tokio::test(flavor = "multi_thread")]
10755 async fn test_coincident_of_line_point_and_circle_segment() {
10756 let initial_source = "\
10757sketch(on = XY) {
10758 circle1 = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
10759 line1 = line(start = [var 9mm, var 1mm], end = [var 10mm, var 2mm])
10760}
10761";
10762 let program = Program::parse(initial_source).unwrap().0.unwrap();
10763 let mut frontend = FrontendState::new();
10764
10765 let mock_ctx = ExecutorContext::new_mock(None).await;
10766 let version = Version(0);
10767
10768 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10769 frontend.program = program;
10770 frontend.update_state_after_exec(outcome, true);
10771 let sketch_object = find_first_sketch_object(&frontend.scene_graph).expect("Expected sketch object");
10772 let sketch_id = sketch_object.id;
10773 let sketch = expect_sketch(sketch_object);
10774
10775 let circle_id = sketch
10776 .segments
10777 .iter()
10778 .copied()
10779 .find(|seg_id| {
10780 matches!(
10781 &frontend.scene_graph.objects[seg_id.0].kind,
10782 ObjectKind::Segment {
10783 segment: Segment::Circle(_)
10784 }
10785 )
10786 })
10787 .expect("Expected a circle segment in sketch");
10788 let line_id = frontend
10789 .scene_graph
10790 .objects
10791 .iter()
10792 .find_map(|obj| match &obj.kind {
10793 ObjectKind::Segment {
10794 segment: Segment::Line(line),
10795 } if line.owner.is_none() => Some(obj.id),
10796 _ => None,
10797 })
10798 .expect("Expected a standalone line segment in scene graph");
10799
10800 let line_start_point_id = match &frontend.scene_graph.objects[line_id.0].kind {
10801 ObjectKind::Segment {
10802 segment: Segment::Line(line),
10803 } => line.start,
10804 _ => panic!("Expected line segment object"),
10805 };
10806
10807 let constraint = Constraint::Coincident(Coincident {
10808 segments: vec![line_start_point_id.into(), circle_id.into()],
10809 });
10810 let (src_delta, _scene_delta) = frontend
10811 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10812 .await
10813 .unwrap();
10814 insta::assert_snapshot!(
10815 "test_coincident_of_line_point_and_circle_segment",
10816 src_delta.text.as_str()
10817 );
10818
10819 mock_ctx.close().await;
10820 }
10821
10822 #[tokio::test(flavor = "multi_thread")]
10823 async fn test_invalid_coincident_arc_and_line_preserves_state() {
10824 let program = Program::empty();
10832
10833 let mut frontend = FrontendState::new();
10834 frontend.program = program;
10835
10836 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
10837 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
10838 let version = Version(0);
10839
10840 let sketch_args = SketchCtor {
10841 on: Plane::Default(PlaneName::Xy),
10842 };
10843 let (_src_delta, _scene_delta, sketch_id) = frontend
10844 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
10845 .await
10846 .unwrap();
10847
10848 let arc_ctor = ArcCtor {
10850 start: Point2d {
10851 x: Expr::Var(Number {
10852 value: 0.0,
10853 units: NumericSuffix::Mm,
10854 }),
10855 y: Expr::Var(Number {
10856 value: 0.0,
10857 units: NumericSuffix::Mm,
10858 }),
10859 },
10860 end: Point2d {
10861 x: Expr::Var(Number {
10862 value: 10.0,
10863 units: NumericSuffix::Mm,
10864 }),
10865 y: Expr::Var(Number {
10866 value: 10.0,
10867 units: NumericSuffix::Mm,
10868 }),
10869 },
10870 center: Point2d {
10871 x: Expr::Var(Number {
10872 value: 10.0,
10873 units: NumericSuffix::Mm,
10874 }),
10875 y: Expr::Var(Number {
10876 value: 0.0,
10877 units: NumericSuffix::Mm,
10878 }),
10879 },
10880 direction: None,
10881 construction: None,
10882 };
10883 let (_src_delta, scene_delta) = frontend
10884 .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Arc(arc_ctor), None)
10885 .await
10886 .unwrap();
10887 let arc_id = *scene_delta.new_objects.last().unwrap();
10889
10890 let line_ctor = LineCtor {
10892 start: Point2d {
10893 x: Expr::Var(Number {
10894 value: 20.0,
10895 units: NumericSuffix::Mm,
10896 }),
10897 y: Expr::Var(Number {
10898 value: 0.0,
10899 units: NumericSuffix::Mm,
10900 }),
10901 },
10902 end: Point2d {
10903 x: Expr::Var(Number {
10904 value: 30.0,
10905 units: NumericSuffix::Mm,
10906 }),
10907 y: Expr::Var(Number {
10908 value: 10.0,
10909 units: NumericSuffix::Mm,
10910 }),
10911 },
10912 construction: None,
10913 };
10914 let (_src_delta, scene_delta) = frontend
10915 .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Line(line_ctor), None)
10916 .await
10917 .unwrap();
10918 let line_id = *scene_delta.new_objects.last().unwrap();
10920
10921 let constraint = Constraint::Coincident(Coincident {
10924 segments: vec![arc_id.into(), line_id.into()],
10925 });
10926 let result = frontend.add_constraint(&mock_ctx, version, sketch_id, constraint).await;
10927
10928 assert!(result.is_err(), "Expected invalid coincident constraint to fail");
10930
10931 let sketch_object_after =
10934 find_first_sketch_object(&frontend.scene_graph).expect("Sketch should still exist after failed constraint");
10935 let sketch_after = expect_sketch(sketch_object_after);
10936
10937 assert!(
10939 sketch_after.segments.contains(&arc_id),
10940 "Arc segment should still exist after failed constraint"
10941 );
10942 assert!(
10943 sketch_after.segments.contains(&line_id),
10944 "Line segment should still exist after failed constraint"
10945 );
10946
10947 let arc_obj = frontend
10949 .scene_graph
10950 .objects
10951 .get(arc_id.0)
10952 .expect("Arc object should still be accessible");
10953 let line_obj = frontend
10954 .scene_graph
10955 .objects
10956 .get(line_id.0)
10957 .expect("Line object should still be accessible");
10958
10959 match &arc_obj.kind {
10962 ObjectKind::Segment {
10963 segment: Segment::Arc(_),
10964 } => {}
10965 _ => panic!("Arc object should still be an arc segment"),
10966 }
10967 match &line_obj.kind {
10968 ObjectKind::Segment {
10969 segment: Segment::Line(_),
10970 } => {}
10971 _ => panic!("Line object should still be a line segment"),
10972 }
10973
10974 ctx.close().await;
10975 mock_ctx.close().await;
10976 }
10977
10978 #[tokio::test(flavor = "multi_thread")]
10979 async fn test_distance_two_points() {
10980 let initial_source = "\
10981sketch(on = XY) {
10982 point(at = [var 1, var 2])
10983 point(at = [var 3, var 4])
10984}
10985";
10986
10987 let program = Program::parse(initial_source).unwrap().0.unwrap();
10988
10989 let mut frontend = FrontendState::new();
10990
10991 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
10992 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
10993 let version = Version(0);
10994
10995 frontend.hack_set_program(&ctx, program).await.unwrap();
10996 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10997 let sketch_id = sketch_object.id;
10998 let sketch = expect_sketch(sketch_object);
10999 let point0_id = *sketch.segments.first().unwrap();
11000 let point1_id = *sketch.segments.get(1).unwrap();
11001
11002 let constraint = Constraint::Distance(Distance {
11003 segments: vec![point0_id.into(), point1_id.into()],
11004 distance: Number {
11005 value: 2.0,
11006 units: NumericSuffix::Mm,
11007 },
11008 label_position: None,
11009 source: Default::default(),
11010 });
11011 let (src_delta, scene_delta) = frontend
11012 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11013 .await
11014 .unwrap();
11015 insta::assert_snapshot!("test_distance_two_points", src_delta.text.as_str());
11016 assert_eq!(
11017 scene_delta.new_graph.objects.len(),
11018 5,
11019 "{:#?}",
11020 scene_delta.new_graph.objects
11021 );
11022
11023 ctx.close().await;
11024 mock_ctx.close().await;
11025 }
11026
11027 #[tokio::test(flavor = "multi_thread")]
11028 async fn test_distance_two_points_with_label() {
11029 let initial_source = "\
11030sketch(on = XY) {
11031 point(at = [var 1, var 2])
11032 point(at = [var 3, var 4])
11033}
11034";
11035
11036 let program = Program::parse(initial_source).unwrap().0.unwrap();
11037
11038 let mut frontend = FrontendState::new();
11039
11040 let mock_ctx = ExecutorContext::new_mock(None).await;
11041 let version = Version(0);
11042
11043 frontend.program = program.clone();
11044 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11045 frontend.update_state_after_exec(outcome, true);
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 point0_id = *sketch.segments.first().unwrap();
11050 let point1_id = *sketch.segments.get(1).unwrap();
11051
11052 let label_position = Point2d {
11053 x: Number {
11054 value: 10.0,
11055 units: NumericSuffix::Mm,
11056 },
11057 y: Number {
11058 value: 11.0,
11059 units: NumericSuffix::Mm,
11060 },
11061 };
11062 let constraint = Constraint::Distance(Distance {
11063 segments: vec![point0_id.into(), point1_id.into()],
11064 distance: Number {
11065 value: 2.0,
11066 units: NumericSuffix::Mm,
11067 },
11068 label_position: Some(label_position.clone()),
11069 source: Default::default(),
11070 });
11071 let (src_delta, scene_delta) = frontend
11072 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11073 .await
11074 .unwrap();
11075 insta::assert_snapshot!("test_distance_two_points_with_label", src_delta.text.as_str());
11076
11077 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
11078 let sketch = expect_sketch(sketch_object);
11079 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
11080 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11081 panic!("Expected constraint object");
11082 };
11083 let Constraint::Distance(distance) = constraint else {
11084 panic!("Expected distance constraint");
11085 };
11086 assert_eq!(distance.label_position, Some(label_position));
11087
11088 mock_ctx.close().await;
11089 }
11090
11091 #[tokio::test(flavor = "multi_thread")]
11092 async fn test_edit_distance_constraint_label_position() {
11093 let initial_source = "\
11094sketch(on = XY) {
11095 point(at = [var 1, var 2])
11096 point(at = [var 3, var 2])
11097}
11098";
11099
11100 let program = Program::parse(initial_source).unwrap().0.unwrap();
11101
11102 let mut frontend = FrontendState::new();
11103
11104 let mock_ctx = ExecutorContext::new_mock(None).await;
11105 let version = Version(0);
11106
11107 frontend.program = program.clone();
11108 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11109 frontend.update_state_after_exec(outcome, true);
11110 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11111 let sketch_id = sketch_object.id;
11112 let sketch = expect_sketch(sketch_object);
11113 let point0_id = *sketch.segments.first().unwrap();
11114 let point1_id = *sketch.segments.get(1).unwrap();
11115
11116 let constraint = Constraint::Distance(Distance {
11117 segments: vec![point0_id.into(), point1_id.into()],
11118 distance: Number {
11119 value: 2.0,
11120 units: NumericSuffix::Mm,
11121 },
11122 label_position: None,
11123 source: Default::default(),
11124 });
11125 let (_, scene_delta) = frontend
11126 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11127 .await
11128 .unwrap();
11129 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
11130 let sketch = expect_sketch(sketch_object);
11131 let constraint_id = sketch.constraints[0];
11132 let label_position = Point2d {
11133 x: Number {
11134 value: 10.0,
11135 units: NumericSuffix::Mm,
11136 },
11137 y: Number {
11138 value: 11.0,
11139 units: NumericSuffix::Mm,
11140 },
11141 };
11142
11143 let (src_delta, scene_delta) = frontend
11144 .edit_distance_constraint_label_position(
11145 &mock_ctx,
11146 version,
11147 sketch_id,
11148 constraint_id,
11149 label_position.clone(),
11150 vec![],
11151 )
11152 .await
11153 .unwrap();
11154 insta::assert_snapshot!("test_edit_distance_constraint_label_position", src_delta.text.as_str());
11155
11156 let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
11157 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11158 panic!("Expected constraint object");
11159 };
11160 let Constraint::Distance(distance) = constraint else {
11161 panic!("Expected distance constraint");
11162 };
11163 assert_eq!(distance.label_position, Some(label_position));
11164
11165 mock_ctx.close().await;
11166 }
11167
11168 #[tokio::test(flavor = "multi_thread")]
11169 async fn test_edit_distance_constraint_type_and_value() {
11170 let initial_source = "\
11171sketch(on = XY) {
11172 line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 3mm])
11173 distance([line1.start, line1.end]) == 5mm
11174}
11175";
11176
11177 let program = Program::parse(initial_source).unwrap().0.unwrap();
11178 let mut frontend = FrontendState::new();
11179 let mock_ctx = ExecutorContext::new_mock(None).await;
11180 let version = Version(0);
11181
11182 frontend.program = program.clone();
11183 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11184 frontend.update_state_after_exec(outcome, true);
11185 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11186 let sketch_id = sketch_object.id;
11187 let sketch = expect_sketch(sketch_object);
11188 let constraint_id = sketch.constraints[0];
11189 let point0_id = sketch.segments[0];
11190 let point1_id = sketch.segments[1];
11191 let label_position = Point2d {
11192 x: Number {
11193 value: 2.0,
11194 units: NumericSuffix::Mm,
11195 },
11196 y: Number {
11197 value: 5.0,
11198 units: NumericSuffix::Mm,
11199 },
11200 };
11201
11202 let (source_delta, scene_delta) = frontend
11203 .edit_distance_constraint_with_options(
11204 &mock_ctx,
11205 version,
11206 sketch_id,
11207 constraint_id,
11208 Constraint::HorizontalDistance(Distance {
11209 segments: vec![point0_id.into(), point1_id.into()],
11210 distance: Number {
11211 value: 4.0,
11212 units: NumericSuffix::Mm,
11213 },
11214 label_position: Some(label_position.clone()),
11215 source: Default::default(),
11216 }),
11217 EditConstraintOptions {
11218 commit_solved_initial_guesses: false,
11219 },
11220 )
11221 .await
11222 .unwrap();
11223 assert_eq!(
11224 source_delta.text,
11225 "\
11226sketch(on = XY) {
11227 line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 3mm])
11228 horizontalDistance([line1.start, line1.end], labelPosition = [2mm, 5mm]) == 4mm
11229}
11230"
11231 );
11232
11233 let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
11234 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11235 panic!("Expected constraint object");
11236 };
11237 let Constraint::HorizontalDistance(distance) = constraint else {
11238 panic!("Expected horizontal distance constraint");
11239 };
11240 assert_eq!(distance.distance.value, 4.0);
11241 assert_eq!(distance.label_position, Some(label_position));
11242
11243 mock_ctx.close().await;
11244 }
11245
11246 #[tokio::test(flavor = "multi_thread")]
11247 async fn test_edit_angle_constraint_label_position() {
11248 let initial_source = "\
11249sketch(on = XY) {
11250 line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11251 line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
11252 angle([line1, line2]) == 60deg
11253}
11254";
11255
11256 let program = Program::parse(initial_source).unwrap().0.unwrap();
11257 let mut frontend = FrontendState::new();
11258 let mock_ctx = ExecutorContext::new_mock(None).await;
11259 let version = Version(0);
11260
11261 frontend.program = program.clone();
11262 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11263 frontend.update_state_after_exec(outcome, true);
11264 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11265 let sketch_id = sketch_object.id;
11266 let sketch = expect_sketch(sketch_object);
11267 let constraint_id = sketch.constraints[0];
11268 let label_position = Point2d {
11269 x: Number {
11270 value: 10.0,
11271 units: NumericSuffix::Mm,
11272 },
11273 y: Number {
11274 value: 11.0,
11275 units: NumericSuffix::Mm,
11276 },
11277 };
11278
11279 let (src_delta, scene_delta) = frontend
11280 .edit_distance_constraint_label_position(
11281 &mock_ctx,
11282 version,
11283 sketch_id,
11284 constraint_id,
11285 label_position.clone(),
11286 vec![],
11287 )
11288 .await
11289 .unwrap();
11290 assert_eq!(
11291 src_delta.text.as_str(),
11292 "\
11293sketch(on = XY) {
11294 line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11295 line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.46mm])
11296 angle([line1, line2], labelPosition = [10mm, 11mm]) == 60deg
11297}
11298"
11299 );
11300
11301 let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
11302 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11303 panic!("Expected constraint object");
11304 };
11305 let Constraint::Angle(angle) = constraint else {
11306 panic!("Expected angle constraint");
11307 };
11308 assert_eq!(angle.label_position, Some(label_position));
11309
11310 mock_ctx.close().await;
11311 }
11312
11313 #[tokio::test(flavor = "multi_thread")]
11314 async fn test_edit_angle_constraint_label_position_with_call_on_right() {
11315 let initial_source = "\
11316sketch(on = XY) {
11317 line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11318 line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
11319 60deg == angleDimension(lines = [line1, line2], sector = 1)
11320}
11321";
11322
11323 let program = Program::parse(initial_source).unwrap().0.unwrap();
11324 let mut frontend = FrontendState::new();
11325 let mock_ctx = ExecutorContext::new_mock(None).await;
11326 let version = Version(0);
11327
11328 frontend.program = program.clone();
11329 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11330 frontend.update_state_after_exec(outcome, true);
11331 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11332 let sketch_id = sketch_object.id;
11333 let sketch = expect_sketch(sketch_object);
11334 let constraint_id = sketch.constraints[0];
11335 let label_position = Point2d {
11336 x: Number {
11337 value: 10.0,
11338 units: NumericSuffix::Mm,
11339 },
11340 y: Number {
11341 value: 11.0,
11342 units: NumericSuffix::Mm,
11343 },
11344 };
11345
11346 let (src_delta, scene_delta) = frontend
11347 .edit_distance_constraint_label_position(
11348 &mock_ctx,
11349 version,
11350 sketch_id,
11351 constraint_id,
11352 label_position.clone(),
11353 vec![],
11354 )
11355 .await
11356 .unwrap();
11357 assert_eq!(
11358 src_delta.text.as_str(),
11359 "\
11360sketch(on = XY) {
11361 line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11362 line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.46mm])
11363 60deg == angleDimension(lines = [line1, line2], sector = 1, labelPosition = [10mm, 11mm])
11364}
11365"
11366 );
11367
11368 let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
11369 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11370 panic!("Expected constraint object");
11371 };
11372 let Constraint::Angle(angle) = constraint else {
11373 panic!("Expected angle constraint");
11374 };
11375 assert_eq!(angle.label_position, Some(label_position));
11376
11377 mock_ctx.close().await;
11378 }
11379
11380 #[tokio::test(flavor = "multi_thread")]
11381 async fn test_edit_angle_constraint() {
11382 let initial_source = "\
11383sketch(on = XY) {
11384 line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11385 line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
11386 angle([line1, line2]) == 60deg
11387}
11388";
11389
11390 let program = Program::parse(initial_source).unwrap().0.unwrap();
11391 let mut frontend = FrontendState::new();
11392 let mock_ctx = ExecutorContext::new_mock(None).await;
11393 let version = Version(0);
11394
11395 frontend.program = program.clone();
11396 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11397 frontend.update_state_after_exec(outcome, true);
11398 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11399 let sketch_id = sketch_object.id;
11400 let sketch = expect_sketch(sketch_object);
11401 let constraint_id = sketch.constraints[0];
11402 let line1_id = *sketch.segments.get(2).unwrap();
11403 let line2_id = *sketch.segments.get(5).unwrap();
11404 let label_position = Point2d {
11405 x: Number {
11406 value: 10.0,
11407 units: NumericSuffix::Mm,
11408 },
11409 y: Number {
11410 value: 11.0,
11411 units: NumericSuffix::Mm,
11412 },
11413 };
11414
11415 let (src_delta, scene_delta) = frontend
11416 .edit_angle_constraint_with_options(
11417 &mock_ctx,
11418 version,
11419 sketch_id,
11420 constraint_id,
11421 Angle {
11422 lines: vec![line2_id, line1_id],
11423 angle: Number {
11424 value: 60.0,
11425 units: NumericSuffix::Deg,
11426 },
11427 sector: Some(3),
11428 inverse: Some(false),
11429 label_position: Some(label_position.clone()),
11430 source: Default::default(),
11431 },
11432 EditConstraintOptions {
11433 commit_solved_initial_guesses: false,
11434 },
11435 )
11436 .await
11437 .unwrap();
11438 assert_eq!(
11439 src_delta.text.as_str(),
11440 "\
11441sketch(on = XY) {
11442 line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11443 line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
11444 angleDimension(lines = [line2, line1], sector = 3, labelPosition = [10mm, 11mm]) == 60deg
11445}
11446"
11447 );
11448
11449 let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
11450 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11451 panic!("Expected constraint object");
11452 };
11453 let Constraint::Angle(angle) = constraint else {
11454 panic!("Expected angle constraint");
11455 };
11456 assert_eq!(angle.lines, vec![line2_id, line1_id]);
11457 assert_eq!(angle.sector, Some(3));
11458 assert_eq!(angle.inverse, Some(false));
11459 assert_eq!(angle.label_position, Some(label_position));
11460
11461 mock_ctx.close().await;
11462 }
11463
11464 #[tokio::test(flavor = "multi_thread")]
11465 async fn test_edit_angle_constraint_with_call_on_right() {
11466 let initial_source = "\
11467sketch(on = XY) {
11468 line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11469 line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
11470 60deg == angle([line1, line2])
11471}
11472";
11473
11474 let program = Program::parse(initial_source).unwrap().0.unwrap();
11475 let mut frontend = FrontendState::new();
11476 let mock_ctx = ExecutorContext::new_mock(None).await;
11477 let version = Version(0);
11478
11479 frontend.program = program.clone();
11480 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11481 frontend.update_state_after_exec(outcome, true);
11482 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11483 let sketch_id = sketch_object.id;
11484 let sketch = expect_sketch(sketch_object);
11485 let constraint_id = sketch.constraints[0];
11486 let line1_id = *sketch.segments.get(2).unwrap();
11487 let line2_id = *sketch.segments.get(5).unwrap();
11488
11489 let (src_delta, _) = frontend
11490 .edit_angle_constraint_with_options(
11491 &mock_ctx,
11492 version,
11493 sketch_id,
11494 constraint_id,
11495 Angle {
11496 lines: vec![line2_id, line1_id],
11497 angle: Number {
11498 value: 60.0,
11499 units: NumericSuffix::Deg,
11500 },
11501 sector: Some(3),
11502 inverse: Some(false),
11503 label_position: None,
11504 source: Default::default(),
11505 },
11506 EditConstraintOptions {
11507 commit_solved_initial_guesses: false,
11508 },
11509 )
11510 .await
11511 .unwrap();
11512 assert_eq!(
11513 src_delta.text.as_str(),
11514 "\
11515sketch(on = XY) {
11516 line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11517 line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
11518 60deg == angleDimension(lines = [line2, line1], sector = 3)
11519}
11520"
11521 );
11522
11523 mock_ctx.close().await;
11524 }
11525
11526 #[tokio::test(flavor = "multi_thread")]
11527 async fn test_edit_segments_can_commit_constraint_label_position_in_same_execution() {
11528 let initial_source = "\
11529@settings(kclVersion = 2.0)
11530
11531sketch001 = sketch(on = XZ) {
11532 line1 = line(start = [var 0mm, var 12.55mm], end = [var -6.03mm, var 8.51mm])
11533 line3 = line(start = [var -7.41mm, var 2.92mm], end = [var -1.47mm, var 4.32mm])
11534 distance([line1.start, line3.end], labelPosition = [5.56mm, 8.65mm]) == 8.36mm
11535 vertical([line1.start, ORIGIN])
11536}
11537";
11538
11539 let program = Program::parse(initial_source).unwrap().0.unwrap();
11540 let mut frontend = FrontendState::new();
11541 let mock_ctx = ExecutorContext::new_mock(None).await;
11542 let version = Version(0);
11543
11544 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
11545 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11546 let sketch_id = sketch_object.id;
11547 let sketch = expect_sketch(sketch_object);
11548 let constraint_id = sketch
11549 .constraints
11550 .iter()
11551 .copied()
11552 .find(|constraint_id| {
11553 matches!(
11554 frontend.scene_graph.objects[constraint_id.0].kind,
11555 ObjectKind::Constraint {
11556 constraint: Constraint::Distance(_)
11557 }
11558 )
11559 })
11560 .unwrap();
11561 let line1_id = sketch
11562 .segments
11563 .iter()
11564 .copied()
11565 .find(|segment_id| {
11566 matches!(
11567 frontend.scene_graph.objects[segment_id.0].kind,
11568 ObjectKind::Segment {
11569 segment: Segment::Line(_)
11570 }
11571 )
11572 })
11573 .unwrap();
11574 let label_position = Point2d {
11575 x: Number {
11576 value: 7.0,
11577 units: NumericSuffix::Mm,
11578 },
11579 y: Number {
11580 value: 9.0,
11581 units: NumericSuffix::Mm,
11582 },
11583 };
11584
11585 let (source_delta, scene_delta) = frontend
11586 .edit_segments_with_options(
11587 &mock_ctx,
11588 version,
11589 sketch_id,
11590 vec![ExistingSegmentCtor {
11591 id: line1_id,
11592 ctor: SegmentCtor::Line(LineCtor {
11593 start: point_expr_mm(2.0, 15.55),
11594 end: point_expr_mm(-4.03, 11.51),
11595 construction: None,
11596 }),
11597 }],
11598 EditSegmentsOptions {
11599 anchor_segment_ids: Some(vec![]),
11600 drag_anchors: vec![SegmentDragAnchor {
11601 segment_id: line1_id,
11602 target: label_position.clone(),
11603 }],
11604 constraint_label_edits: vec![ConstraintLabelPositionEdit {
11605 constraint_id,
11606 label_position: label_position.clone(),
11607 }],
11608 commit_solved_initial_guesses: true,
11609 },
11610 )
11611 .await
11612 .unwrap();
11613
11614 assert!(source_delta.text.contains("labelPosition = [7mm, 9mm]"));
11615 let constraint_object = &scene_delta.new_graph.objects[constraint_id.0];
11616 let ObjectKind::Constraint {
11617 constraint: Constraint::Distance(distance),
11618 } = &constraint_object.kind
11619 else {
11620 panic!("Expected distance constraint object");
11621 };
11622 assert_eq!(distance.label_position, Some(label_position));
11623
11624 let snapped_label_position = Point2d {
11625 x: Number {
11626 value: 8.0,
11627 units: NumericSuffix::Mm,
11628 },
11629 y: Number {
11630 value: 10.0,
11631 units: NumericSuffix::Mm,
11632 },
11633 };
11634 let (source_delta, scene_delta) = frontend
11635 .edit_segments_with_options(
11636 &mock_ctx,
11637 version,
11638 sketch_id,
11639 vec![],
11640 EditSegmentsOptions {
11641 anchor_segment_ids: Some(vec![line1_id]),
11642 drag_anchors: vec![],
11643 constraint_label_edits: vec![ConstraintLabelPositionEdit {
11644 constraint_id,
11645 label_position: snapped_label_position.clone(),
11646 }],
11647 commit_solved_initial_guesses: true,
11648 },
11649 )
11650 .await
11651 .unwrap();
11652
11653 assert!(source_delta.text.contains("labelPosition = [8mm, 10mm]"));
11654 let constraint_object = &scene_delta.new_graph.objects[constraint_id.0];
11655 let ObjectKind::Constraint {
11656 constraint: Constraint::Distance(distance),
11657 } = &constraint_object.kind
11658 else {
11659 panic!("Expected distance constraint object");
11660 };
11661 assert_eq!(distance.label_position, Some(snapped_label_position));
11662
11663 mock_ctx.close().await;
11664 }
11665
11666 #[tokio::test(flavor = "multi_thread")]
11667 async fn test_edit_distance_constraint_label_position_preserves_anchor_segment_solution() {
11668 let initial_source = "\
11669sketch(on = XY) {
11670 point1 = point(at = [var 0mm, var 0mm])
11671 point2 = point(at = [var 10mm, var 0mm])
11672 distance([point1, point2]) == 5mm
11673}
11674";
11675
11676 let program = Program::parse(initial_source).unwrap().0.unwrap();
11677 let mut frontend = FrontendState::new();
11678 let mock_ctx = ExecutorContext::new_mock(None).await;
11679 let version = Version(0);
11680
11681 frontend.program = program.clone();
11682 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11683 frontend.update_state_after_exec(outcome, true);
11684 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11685 let sketch_id = sketch_object.id;
11686 let sketch = expect_sketch(sketch_object);
11687 let point0_id = sketch.segments[0];
11688 let point1_id = sketch.segments[1];
11689 let constraint_id = sketch.constraints[0];
11690
11691 let edited_segments = vec![ExistingSegmentCtor {
11692 id: point0_id,
11693 ctor: SegmentCtor::Point(PointCtor {
11694 position: Point2d {
11695 x: Expr::Var(Number {
11696 value: 2.0,
11697 units: NumericSuffix::Mm,
11698 }),
11699 y: Expr::Var(Number {
11700 value: 1.0,
11701 units: NumericSuffix::Mm,
11702 }),
11703 },
11704 }),
11705 }];
11706 let (_, scene_delta) = frontend
11707 .edit_segments(&mock_ctx, version, sketch_id, edited_segments)
11708 .await
11709 .unwrap();
11710 let point0_after_segment_edit = point_position(&scene_delta.new_graph, point0_id);
11711 let point1_after_segment_edit = point_position(&scene_delta.new_graph, point1_id);
11712
11713 let label_position = Point2d {
11714 x: Number {
11715 value: 3.0,
11716 units: NumericSuffix::Mm,
11717 },
11718 y: Number {
11719 value: 4.0,
11720 units: NumericSuffix::Mm,
11721 },
11722 };
11723 let (_, scene_delta) = frontend
11724 .edit_distance_constraint_label_position(
11725 &mock_ctx,
11726 version,
11727 sketch_id,
11728 constraint_id,
11729 label_position,
11730 vec![point0_id],
11731 )
11732 .await
11733 .unwrap();
11734
11735 assert_point_position_close(
11736 point_position(&scene_delta.new_graph, point0_id),
11737 point0_after_segment_edit,
11738 );
11739 assert_point_position_close(
11740 point_position(&scene_delta.new_graph, point1_id),
11741 point1_after_segment_edit,
11742 );
11743
11744 mock_ctx.close().await;
11745 }
11746
11747 #[tokio::test(flavor = "multi_thread")]
11748 async fn test_distance_point_line() {
11749 let initial_source = "\
11750sketch(on = XY) {
11751 point(at = [var 0, var 5])
11752 line(start = [var 0, var 0], end = [var 10, var 0])
11753}
11754";
11755
11756 let program = Program::parse(initial_source).unwrap().0.unwrap();
11757
11758 let mut frontend = FrontendState::new();
11759
11760 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
11761 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
11762 let version = Version(0);
11763
11764 let outcome = frontend.hack_set_program(&ctx, program).await.unwrap();
11765 assert!(matches!(outcome, SetProgramOutcome::Success { .. }), "{outcome:?}");
11766 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11767 let sketch_id = sketch_object.id;
11768 let sketch = expect_sketch(sketch_object);
11769 let point_id = *sketch.segments.first().unwrap();
11770 let line_id = *sketch
11771 .segments
11772 .iter()
11773 .find(|segment_id| {
11774 matches!(
11775 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11776 Some(ObjectKind::Segment {
11777 segment: Segment::Line(_)
11778 })
11779 )
11780 })
11781 .unwrap();
11782
11783 let label_position = Point2d {
11784 x: Number {
11785 value: 10.0,
11786 units: NumericSuffix::Mm,
11787 },
11788 y: Number {
11789 value: 11.0,
11790 units: NumericSuffix::Mm,
11791 },
11792 };
11793 let constraint = Constraint::Distance(Distance {
11794 segments: vec![point_id.into(), line_id.into()],
11795 distance: Number {
11796 value: 5.0,
11797 units: NumericSuffix::Mm,
11798 },
11799 label_position: Some(label_position.clone()),
11800 source: Default::default(),
11801 });
11802 let (src_delta, scene_delta) = frontend
11803 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11804 .await
11805 .unwrap();
11806 insta::assert_snapshot!("test_distance_point_line", src_delta.text.as_str());
11807 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
11808 let sketch = expect_sketch(sketch_object);
11809 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
11810 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11811 panic!("Expected constraint object");
11812 };
11813 let Constraint::Distance(distance) = constraint else {
11814 panic!("Expected distance constraint");
11815 };
11816 assert_eq!(distance.label_position, Some(label_position));
11817
11818 ctx.close().await;
11819 mock_ctx.close().await;
11820 }
11821
11822 #[tokio::test(flavor = "multi_thread")]
11823 async fn test_distance_point_arc() {
11824 let initial_source = "\
11825sketch(on = XY) {
11826 point(at = [var 0, var 8])
11827 arc(start = [var 5, var 0], end = [var 0, var 5], center = [var 0, var 0])
11828}
11829";
11830
11831 let program = Program::parse(initial_source).unwrap().0.unwrap();
11832
11833 let mut frontend = FrontendState::new();
11834
11835 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
11836 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
11837 let version = Version(0);
11838
11839 frontend.hack_set_program(&ctx, program).await.unwrap();
11840 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11841 let sketch_id = sketch_object.id;
11842 let sketch = expect_sketch(sketch_object);
11843 let point_id = *sketch.segments.first().unwrap();
11844 let arc_id = *sketch
11845 .segments
11846 .iter()
11847 .find(|segment_id| {
11848 matches!(
11849 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11850 Some(ObjectKind::Segment {
11851 segment: Segment::Arc(_)
11852 })
11853 )
11854 })
11855 .unwrap();
11856
11857 let constraint = Constraint::Distance(Distance {
11858 segments: vec![point_id.into(), arc_id.into()],
11859 distance: Number {
11860 value: 3.0,
11861 units: NumericSuffix::Mm,
11862 },
11863 label_position: None,
11864 source: Default::default(),
11865 });
11866 let (src_delta, _scene_delta) = frontend
11867 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11868 .await
11869 .unwrap();
11870 insta::assert_snapshot!("test_distance_point_arc", src_delta.text.as_str());
11871
11872 ctx.close().await;
11873 mock_ctx.close().await;
11874 }
11875
11876 #[tokio::test(flavor = "multi_thread")]
11877 async fn test_distance_arc_origin() {
11878 let initial_source = "\
11879sketch001 = sketch(on = XY) {
11880 arc(start = [var -4.13mm, var -0.59mm], end = [var -3.47mm, var 3.38mm], center = [var -4.55mm, var 1.52mm])
11881}
11882";
11883
11884 let program = Program::parse(initial_source).unwrap().0.unwrap();
11885
11886 let mut frontend = FrontendState::new();
11887
11888 let mock_ctx = ExecutorContext::new_mock(None).await;
11889 let version = Version(0);
11890
11891 frontend.program = program.clone();
11892 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11893 frontend.update_state_after_exec(outcome, true);
11894 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11895 let sketch_id = sketch_object.id;
11896 let sketch = expect_sketch(sketch_object);
11897 let arc_id = *sketch
11898 .segments
11899 .iter()
11900 .find(|segment_id| {
11901 matches!(
11902 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11903 Some(ObjectKind::Segment {
11904 segment: Segment::Arc(_)
11905 })
11906 )
11907 })
11908 .unwrap();
11909
11910 let constraint = Constraint::Distance(Distance {
11911 segments: vec![arc_id.into(), ConstraintSegment::ORIGIN],
11912 distance: Number {
11913 value: 3.0,
11914 units: NumericSuffix::Mm,
11915 },
11916 label_position: None,
11917 source: Default::default(),
11918 });
11919 let (src_delta, _scene_delta) = frontend
11920 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11921 .await
11922 .unwrap();
11923 insta::assert_snapshot!("test_distance_arc_origin", src_delta.text.as_str());
11924
11925 mock_ctx.close().await;
11926 }
11927
11928 #[tokio::test(flavor = "multi_thread")]
11929 async fn test_distance_line_origin() {
11930 let initial_source = "\
11931sketch(on = XY) {
11932 line(start = [var 5, var 0], end = [var 5, var 10])
11933}
11934";
11935
11936 let program = Program::parse(initial_source).unwrap().0.unwrap();
11937
11938 let mut frontend = FrontendState::new();
11939
11940 let mock_ctx = ExecutorContext::new_mock(None).await;
11941 let version = Version(0);
11942
11943 frontend.program = program.clone();
11944 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11945 frontend.update_state_after_exec(outcome, true);
11946 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11947 let sketch_id = sketch_object.id;
11948 let sketch = expect_sketch(sketch_object);
11949 let line_id = *sketch
11950 .segments
11951 .iter()
11952 .find(|segment_id| {
11953 matches!(
11954 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11955 Some(ObjectKind::Segment {
11956 segment: Segment::Line(_)
11957 })
11958 )
11959 })
11960 .unwrap();
11961
11962 let constraint = Constraint::Distance(Distance {
11963 segments: vec![ConstraintSegment::ORIGIN, line_id.into()],
11964 distance: Number {
11965 value: 5.0,
11966 units: NumericSuffix::Mm,
11967 },
11968 label_position: None,
11969 source: Default::default(),
11970 });
11971 let (src_delta, _scene_delta) = frontend
11972 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11973 .await
11974 .unwrap();
11975 insta::assert_snapshot!("test_distance_line_origin", src_delta.text.as_str());
11976
11977 mock_ctx.close().await;
11978 }
11979
11980 #[tokio::test(flavor = "multi_thread")]
11981 async fn test_distance_line_circle() {
11982 let initial_source = "\
11983sketch(on = XY) {
11984 line(start = [var -10, var 8], end = [var 10, var 8])
11985 circle(start = [var 5, var 0], center = [var 0, var 0])
11986}
11987";
11988
11989 let program = Program::parse(initial_source).unwrap().0.unwrap();
11990
11991 let mut frontend = FrontendState::new();
11992
11993 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
11994 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
11995 let version = Version(0);
11996
11997 frontend.hack_set_program(&ctx, program).await.unwrap();
11998 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11999 let sketch_id = sketch_object.id;
12000 let sketch = expect_sketch(sketch_object);
12001 let line_id = *sketch
12002 .segments
12003 .iter()
12004 .find(|segment_id| {
12005 matches!(
12006 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
12007 Some(ObjectKind::Segment {
12008 segment: Segment::Line(_)
12009 })
12010 )
12011 })
12012 .unwrap();
12013 let circle_id = *sketch
12014 .segments
12015 .iter()
12016 .find(|segment_id| {
12017 matches!(
12018 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
12019 Some(ObjectKind::Segment {
12020 segment: Segment::Circle(_)
12021 })
12022 )
12023 })
12024 .unwrap();
12025
12026 let constraint = Constraint::Distance(Distance {
12027 segments: vec![line_id.into(), circle_id.into()],
12028 distance: Number {
12029 value: 3.0,
12030 units: NumericSuffix::Mm,
12031 },
12032 label_position: None,
12033 source: Default::default(),
12034 });
12035 let (src_delta, _scene_delta) = frontend
12036 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12037 .await
12038 .unwrap();
12039 insta::assert_snapshot!("test_distance_line_circle", src_delta.text.as_str());
12040
12041 ctx.close().await;
12042 mock_ctx.close().await;
12043 }
12044
12045 #[tokio::test(flavor = "multi_thread")]
12046 async fn test_distance_circle_arc() {
12047 let initial_source = "\
12048sketch(on = XY) {
12049 circle(start = [var 5, var 0], center = [var 0, var 0])
12050 arc(start = [var 15, var 0], end = [var 10, var 5], center = [var 10, var 0])
12051}
12052";
12053
12054 let program = Program::parse(initial_source).unwrap().0.unwrap();
12055
12056 let mut frontend = FrontendState::new();
12057
12058 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
12059 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
12060 let version = Version(0);
12061
12062 let outcome = frontend.hack_set_program(&ctx, program).await.unwrap();
12063 assert!(matches!(outcome, SetProgramOutcome::Success { .. }), "{outcome:?}");
12064 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12065 let sketch_id = sketch_object.id;
12066 let sketch = expect_sketch(sketch_object);
12067 let circle_id = *sketch
12068 .segments
12069 .iter()
12070 .find(|segment_id| {
12071 matches!(
12072 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
12073 Some(ObjectKind::Segment {
12074 segment: Segment::Circle(_)
12075 })
12076 )
12077 })
12078 .unwrap();
12079 let arc_id = *sketch
12080 .segments
12081 .iter()
12082 .find(|segment_id| {
12083 matches!(
12084 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
12085 Some(ObjectKind::Segment {
12086 segment: Segment::Arc(_)
12087 })
12088 )
12089 })
12090 .unwrap();
12091
12092 let constraint = Constraint::Distance(Distance {
12093 segments: vec![circle_id.into(), arc_id.into()],
12094 distance: Number {
12095 value: 3.0,
12096 units: NumericSuffix::Mm,
12097 },
12098 label_position: None,
12099 source: Default::default(),
12100 });
12101 let (src_delta, _scene_delta) = frontend
12102 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12103 .await
12104 .unwrap();
12105 insta::assert_snapshot!("test_distance_circle_arc", src_delta.text.as_str());
12106
12107 ctx.close().await;
12108 mock_ctx.close().await;
12109 }
12110
12111 #[tokio::test(flavor = "multi_thread")]
12112 async fn test_distance_parallel_lines() {
12113 let initial_source = "\
12114sketch(on = XY) {
12115 line(start = [var 0, var 0], end = [var 10, var 0])
12116 line(start = [var 0, var 5], end = [var 10, var 5])
12117}
12118";
12119
12120 let program = Program::parse(initial_source).unwrap().0.unwrap();
12121
12122 let mut frontend = FrontendState::new();
12123
12124 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
12125 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
12126 let version = Version(0);
12127
12128 frontend.hack_set_program(&ctx, program).await.unwrap();
12129 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12130 let sketch_id = sketch_object.id;
12131 let sketch = expect_sketch(sketch_object);
12132 let line_ids = sketch
12133 .segments
12134 .iter()
12135 .copied()
12136 .filter(|segment_id| {
12137 matches!(
12138 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
12139 Some(ObjectKind::Segment {
12140 segment: Segment::Line(_)
12141 })
12142 )
12143 })
12144 .collect::<Vec<_>>();
12145
12146 let constraint = Constraint::Distance(Distance {
12147 segments: vec![line_ids[0].into(), line_ids[1].into()],
12148 distance: Number {
12149 value: 5.0,
12150 units: NumericSuffix::Mm,
12151 },
12152 label_position: None,
12153 source: Default::default(),
12154 });
12155 let (src_delta, _scene_delta) = frontend
12156 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12157 .await
12158 .unwrap();
12159 insta::assert_snapshot!("test_distance_parallel_lines", src_delta.text.as_str());
12160
12161 ctx.close().await;
12162 mock_ctx.close().await;
12163 }
12164
12165 #[tokio::test(flavor = "multi_thread")]
12166 async fn test_distance_non_parallel_lines_lowers_to_distance() {
12167 let initial_source = "\
12172sketch(on = XY) {
12173 line(start = [var 0, var 0], end = [var 10, var 0])
12174 line(start = [var 0, var 0], end = [var 10, var 10])
12175}
12176";
12177
12178 let program = Program::parse(initial_source).unwrap().0.unwrap();
12179
12180 let mut frontend = FrontendState::new();
12181
12182 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
12183 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
12184 let version = Version(0);
12185
12186 frontend.hack_set_program(&ctx, program).await.unwrap();
12187 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12188 let sketch_id = sketch_object.id;
12189 let sketch = expect_sketch(sketch_object);
12190 let line_ids = sketch
12191 .segments
12192 .iter()
12193 .copied()
12194 .filter(|segment_id| {
12195 matches!(
12196 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
12197 Some(ObjectKind::Segment {
12198 segment: Segment::Line(_)
12199 })
12200 )
12201 })
12202 .collect::<Vec<_>>();
12203
12204 let constraint = Constraint::Distance(Distance {
12205 segments: vec![line_ids[0].into(), line_ids[1].into()],
12206 distance: Number {
12207 value: 5.0,
12208 units: NumericSuffix::Mm,
12209 },
12210 label_position: None,
12211 source: Default::default(),
12212 });
12213 let (src_delta, _scene_delta) = frontend
12214 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12215 .await
12216 .unwrap();
12217 insta::assert_snapshot!(
12218 "test_distance_non_parallel_lines_lowers_to_distance",
12219 src_delta.text.as_str()
12220 );
12221
12222 ctx.close().await;
12223 mock_ctx.close().await;
12224 }
12225
12226 #[tokio::test(flavor = "multi_thread")]
12227 async fn test_horizontal_distance_two_points() {
12228 let initial_source = "\
12229sketch(on = XY) {
12230 point(at = [var 1, var 2])
12231 point(at = [var 3, var 4])
12232}
12233";
12234
12235 let program = Program::parse(initial_source).unwrap().0.unwrap();
12236
12237 let mut frontend = FrontendState::new();
12238
12239 let mock_ctx = ExecutorContext::new_mock(None).await;
12240 let version = Version(0);
12241
12242 frontend.program = program.clone();
12243 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12244 frontend.update_state_after_exec(outcome, true);
12245 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12246 let sketch_id = sketch_object.id;
12247 let sketch = expect_sketch(sketch_object);
12248 let point0_id = *sketch.segments.first().unwrap();
12249 let point1_id = *sketch.segments.get(1).unwrap();
12250 let label_position = Point2d {
12251 x: Number {
12252 value: 10.0,
12253 units: NumericSuffix::Mm,
12254 },
12255 y: Number {
12256 value: 11.0,
12257 units: NumericSuffix::Mm,
12258 },
12259 };
12260
12261 let constraint = Constraint::HorizontalDistance(Distance {
12262 segments: vec![point0_id.into(), point1_id.into()],
12263 distance: Number {
12264 value: 2.0,
12265 units: NumericSuffix::Mm,
12266 },
12267 label_position: Some(label_position.clone()),
12268 source: Default::default(),
12269 });
12270 let (src_delta, scene_delta) = frontend
12271 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12272 .await
12273 .unwrap();
12274 insta::assert_snapshot!("test_horizontal_distance_two_points", src_delta.text.as_str());
12275 assert_eq!(
12276 scene_delta.new_graph.objects.len(),
12277 5,
12278 "{:#?}",
12279 scene_delta.new_graph.objects
12280 );
12281 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
12282 let sketch = expect_sketch(sketch_object);
12283 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
12284 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12285 panic!("Expected constraint object");
12286 };
12287 let Constraint::HorizontalDistance(distance) = constraint else {
12288 panic!("Expected horizontal distance constraint");
12289 };
12290 assert_eq!(distance.label_position, Some(label_position));
12291
12292 mock_ctx.close().await;
12293 }
12294
12295 #[tokio::test(flavor = "multi_thread")]
12296 async fn test_radius_single_arc_segment() {
12297 let initial_source = "\
12298sketch(on = XY) {
12299 arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
12300}
12301";
12302
12303 let program = Program::parse(initial_source).unwrap().0.unwrap();
12304
12305 let mut frontend = FrontendState::new();
12306
12307 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
12308 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
12309 let version = Version(0);
12310
12311 frontend.hack_set_program(&ctx, program).await.unwrap();
12312 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12313 let sketch_id = sketch_object.id;
12314 let sketch = expect_sketch(sketch_object);
12315 let arc_id = sketch
12317 .segments
12318 .iter()
12319 .find(|&seg_id| {
12320 let obj = frontend.scene_graph.objects.get(seg_id.0);
12321 matches!(
12322 obj.map(|o| &o.kind),
12323 Some(ObjectKind::Segment {
12324 segment: Segment::Arc(_)
12325 })
12326 )
12327 })
12328 .unwrap();
12329
12330 let constraint = Constraint::Radius(Radius {
12331 arc: *arc_id,
12332 radius: Number {
12333 value: 5.0,
12334 units: NumericSuffix::Mm,
12335 },
12336 label_position: None,
12337 source: Default::default(),
12338 });
12339 let (src_delta, scene_delta) = frontend
12340 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12341 .await
12342 .unwrap();
12343 insta::assert_snapshot!("test_radius_single_arc_segment", src_delta.text.as_str());
12344 assert_eq!(
12345 scene_delta.new_graph.objects.len(),
12346 7, "{:#?}",
12348 scene_delta.new_graph.objects
12349 );
12350
12351 ctx.close().await;
12352 mock_ctx.close().await;
12353 }
12354
12355 #[tokio::test(flavor = "multi_thread")]
12356 async fn test_radius_single_arc_segment_with_label_position() {
12357 let initial_source = "\
12358sketch(on = XY) {
12359 arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
12360}
12361";
12362
12363 let program = Program::parse(initial_source).unwrap().0.unwrap();
12364 let mut frontend = FrontendState::new();
12365 let mock_ctx = ExecutorContext::new_mock(None).await;
12366 let version = Version(0);
12367
12368 frontend.program = program.clone();
12369 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12370 frontend.update_state_after_exec(outcome, true);
12371 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12372 let sketch_id = sketch_object.id;
12373 let sketch = expect_sketch(sketch_object);
12374 let arc_id = sketch
12375 .segments
12376 .iter()
12377 .find(|&seg_id| {
12378 let obj = frontend.scene_graph.objects.get(seg_id.0);
12379 matches!(
12380 obj.map(|o| &o.kind),
12381 Some(ObjectKind::Segment {
12382 segment: Segment::Arc(_)
12383 })
12384 )
12385 })
12386 .unwrap();
12387
12388 let label_position = Point2d {
12389 x: Number {
12390 value: 10.0,
12391 units: NumericSuffix::Mm,
12392 },
12393 y: Number {
12394 value: 11.0,
12395 units: NumericSuffix::Mm,
12396 },
12397 };
12398 let constraint = Constraint::Radius(Radius {
12399 arc: *arc_id,
12400 radius: Number {
12401 value: 5.0,
12402 units: NumericSuffix::Mm,
12403 },
12404 label_position: Some(label_position.clone()),
12405 source: Default::default(),
12406 });
12407 let (src_delta, scene_delta) = frontend
12408 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12409 .await
12410 .unwrap();
12411 insta::assert_snapshot!(
12412 "test_radius_single_arc_segment_with_label_position",
12413 src_delta.text.as_str()
12414 );
12415
12416 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
12417 let sketch = expect_sketch(sketch_object);
12418 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
12419 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12420 panic!("Expected constraint object");
12421 };
12422 let Constraint::Radius(radius) = constraint else {
12423 panic!("Expected radius constraint");
12424 };
12425 assert_eq!(radius.label_position, Some(label_position));
12426
12427 mock_ctx.close().await;
12428 }
12429
12430 #[tokio::test(flavor = "multi_thread")]
12431 async fn test_edit_radius_constraint_label_position() {
12432 let initial_source = "\
12433sketch(on = XY) {
12434 arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
12435 radius(arc1) == 5mm
12436}
12437";
12438
12439 let program = Program::parse(initial_source).unwrap().0.unwrap();
12440 let mut frontend = FrontendState::new();
12441 let mock_ctx = ExecutorContext::new_mock(None).await;
12442 let version = Version(0);
12443
12444 frontend.program = program.clone();
12445 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12446 frontend.update_state_after_exec(outcome, true);
12447 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12448 let sketch_id = sketch_object.id;
12449 let sketch = expect_sketch(sketch_object);
12450 let constraint_id = sketch.constraints[0];
12451 let label_position = Point2d {
12452 x: Number {
12453 value: 10.0,
12454 units: NumericSuffix::Mm,
12455 },
12456 y: Number {
12457 value: 11.0,
12458 units: NumericSuffix::Mm,
12459 },
12460 };
12461
12462 let (src_delta, scene_delta) = frontend
12463 .edit_distance_constraint_label_position(
12464 &mock_ctx,
12465 version,
12466 sketch_id,
12467 constraint_id,
12468 label_position.clone(),
12469 vec![],
12470 )
12471 .await
12472 .unwrap();
12473 insta::assert_snapshot!("test_edit_radius_constraint_label_position", src_delta.text.as_str());
12474
12475 let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
12476 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12477 panic!("Expected constraint object");
12478 };
12479 let Constraint::Radius(radius) = constraint else {
12480 panic!("Expected radius constraint");
12481 };
12482 assert_eq!(radius.label_position, Some(label_position));
12483
12484 mock_ctx.close().await;
12485 }
12486
12487 #[tokio::test(flavor = "multi_thread")]
12488 async fn test_vertical_distance_two_points() {
12489 let initial_source = "\
12490sketch(on = XY) {
12491 point(at = [var 1, var 2])
12492 point(at = [var 3, var 4])
12493}
12494";
12495
12496 let program = Program::parse(initial_source).unwrap().0.unwrap();
12497
12498 let mut frontend = FrontendState::new();
12499
12500 let mock_ctx = ExecutorContext::new_mock(None).await;
12501 let version = Version(0);
12502
12503 frontend.program = program.clone();
12504 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12505 frontend.update_state_after_exec(outcome, true);
12506 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12507 let sketch_id = sketch_object.id;
12508 let sketch = expect_sketch(sketch_object);
12509 let point0_id = *sketch.segments.first().unwrap();
12510 let point1_id = *sketch.segments.get(1).unwrap();
12511 let label_position = Point2d {
12512 x: Number {
12513 value: 10.0,
12514 units: NumericSuffix::Mm,
12515 },
12516 y: Number {
12517 value: 11.0,
12518 units: NumericSuffix::Mm,
12519 },
12520 };
12521
12522 let constraint = Constraint::VerticalDistance(Distance {
12523 segments: vec![point0_id.into(), point1_id.into()],
12524 distance: Number {
12525 value: 2.0,
12526 units: NumericSuffix::Mm,
12527 },
12528 label_position: Some(label_position.clone()),
12529 source: Default::default(),
12530 });
12531 let (src_delta, scene_delta) = frontend
12532 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12533 .await
12534 .unwrap();
12535 insta::assert_snapshot!("test_vertical_distance_two_points", src_delta.text.as_str());
12536 assert_eq!(
12537 scene_delta.new_graph.objects.len(),
12538 5,
12539 "{:#?}",
12540 scene_delta.new_graph.objects
12541 );
12542 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
12543 let sketch = expect_sketch(sketch_object);
12544 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
12545 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12546 panic!("Expected constraint object");
12547 };
12548 let Constraint::VerticalDistance(distance) = constraint else {
12549 panic!("Expected vertical distance constraint");
12550 };
12551 assert_eq!(distance.label_position, Some(label_position));
12552
12553 mock_ctx.close().await;
12554 }
12555
12556 #[tokio::test(flavor = "multi_thread")]
12557 async fn test_add_fixed_standalone_point() {
12558 let initial_source = "\
12559sketch(on = XY) {
12560 point(at = [var 1, var 2])
12561}
12562";
12563
12564 let program = Program::parse(initial_source).unwrap().0.unwrap();
12565
12566 let mut frontend = FrontendState::new();
12567
12568 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
12569 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
12570 let version = Version(0);
12571
12572 frontend.hack_set_program(&ctx, program).await.unwrap();
12573 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12574 let sketch_id = sketch_object.id;
12575 let sketch = expect_sketch(sketch_object);
12576 let point_id = *sketch.segments.first().unwrap();
12577
12578 let (src_delta, scene_delta) = frontend
12579 .add_constraint(
12580 &mock_ctx,
12581 version,
12582 sketch_id,
12583 Constraint::Fixed(Fixed {
12584 points: vec![FixedPoint {
12585 point: point_id,
12586 position: Point2d {
12587 x: Number {
12588 value: 2.0,
12589 units: NumericSuffix::Mm,
12590 },
12591 y: Number {
12592 value: 3.0,
12593 units: NumericSuffix::Mm,
12594 },
12595 },
12596 }],
12597 }),
12598 )
12599 .await
12600 .unwrap();
12601 insta::assert_snapshot!("test_add_fixed_standalone_point", src_delta.text.as_str());
12602 assert_eq!(
12603 scene_delta.new_graph.objects.len(),
12604 4,
12605 "{:#?}",
12606 scene_delta.new_graph.objects
12607 );
12608
12609 ctx.close().await;
12610 mock_ctx.close().await;
12611 }
12612
12613 #[tokio::test(flavor = "multi_thread")]
12614 async fn test_add_fixed_multiple_points() {
12615 let initial_source = "\
12616sketch(on = XY) {
12617 point(at = [var 1, var 2])
12618 point(at = [var 3, var 4])
12619}
12620";
12621
12622 let program = Program::parse(initial_source).unwrap().0.unwrap();
12623
12624 let mut frontend = FrontendState::new();
12625
12626 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
12627 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
12628 let version = Version(0);
12629
12630 frontend.hack_set_program(&ctx, program).await.unwrap();
12631 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12632 let sketch_id = sketch_object.id;
12633 let sketch = expect_sketch(sketch_object);
12634 let point0_id = *sketch.segments.first().unwrap();
12635 let point1_id = *sketch.segments.get(1).unwrap();
12636
12637 let (src_delta, scene_delta) = frontend
12638 .add_constraint(
12639 &mock_ctx,
12640 version,
12641 sketch_id,
12642 Constraint::Fixed(Fixed {
12643 points: vec![
12644 FixedPoint {
12645 point: point0_id,
12646 position: Point2d {
12647 x: Number {
12648 value: 2.0,
12649 units: NumericSuffix::Mm,
12650 },
12651 y: Number {
12652 value: 3.0,
12653 units: NumericSuffix::Mm,
12654 },
12655 },
12656 },
12657 FixedPoint {
12658 point: point1_id,
12659 position: Point2d {
12660 x: Number {
12661 value: 4.0,
12662 units: NumericSuffix::Mm,
12663 },
12664 y: Number {
12665 value: 5.0,
12666 units: NumericSuffix::Mm,
12667 },
12668 },
12669 },
12670 ],
12671 }),
12672 )
12673 .await
12674 .unwrap();
12675 insta::assert_snapshot!("test_add_fixed_multiple_points", src_delta.text.as_str());
12676 assert_eq!(
12677 scene_delta.new_graph.objects.len(),
12678 6,
12679 "{:#?}",
12680 scene_delta.new_graph.objects
12681 );
12682
12683 ctx.close().await;
12684 mock_ctx.close().await;
12685 }
12686
12687 #[tokio::test(flavor = "multi_thread")]
12688 async fn test_add_fixed_owned_point() {
12689 let initial_source = "\
12690sketch(on = XY) {
12691 line(start = [var 1, var 2], end = [var 3, var 4])
12692}
12693";
12694
12695 let program = Program::parse(initial_source).unwrap().0.unwrap();
12696
12697 let mut frontend = FrontendState::new();
12698
12699 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
12700 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
12701 let version = Version(0);
12702
12703 frontend.hack_set_program(&ctx, program).await.unwrap();
12704 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12705 let sketch_id = sketch_object.id;
12706 let sketch = expect_sketch(sketch_object);
12707 let line_start_id = *sketch.segments.first().unwrap();
12708
12709 let (src_delta, scene_delta) = frontend
12710 .add_constraint(
12711 &mock_ctx,
12712 version,
12713 sketch_id,
12714 Constraint::Fixed(Fixed {
12715 points: vec![FixedPoint {
12716 point: line_start_id,
12717 position: Point2d {
12718 x: Number {
12719 value: 2.0,
12720 units: NumericSuffix::Mm,
12721 },
12722 y: Number {
12723 value: 3.0,
12724 units: NumericSuffix::Mm,
12725 },
12726 },
12727 }],
12728 }),
12729 )
12730 .await
12731 .unwrap();
12732 insta::assert_snapshot!("test_add_fixed_owned_point", src_delta.text.as_str());
12733 assert_eq!(
12734 scene_delta.new_graph.objects.len(),
12735 6,
12736 "{:#?}",
12737 scene_delta.new_graph.objects
12738 );
12739
12740 ctx.close().await;
12741 mock_ctx.close().await;
12742 }
12743
12744 #[tokio::test(flavor = "multi_thread")]
12745 async fn test_radius_error_cases() {
12746 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
12747 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
12748 let version = Version(0);
12749
12750 let initial_source_point = "\
12752sketch(on = XY) {
12753 point(at = [var 1, var 2])
12754}
12755";
12756 let program_point = Program::parse(initial_source_point).unwrap().0.unwrap();
12757 let mut frontend_point = FrontendState::new();
12758 frontend_point.hack_set_program(&ctx, program_point).await.unwrap();
12759 let sketch_object_point = find_first_sketch_object(&frontend_point.scene_graph).unwrap();
12760 let sketch_id_point = sketch_object_point.id;
12761 let sketch_point = expect_sketch(sketch_object_point);
12762 let point_id = *sketch_point.segments.first().unwrap();
12763
12764 let constraint_point = Constraint::Radius(Radius {
12765 arc: point_id,
12766 radius: Number {
12767 value: 5.0,
12768 units: NumericSuffix::Mm,
12769 },
12770 label_position: None,
12771 source: Default::default(),
12772 });
12773 let result_point = frontend_point
12774 .add_constraint(&mock_ctx, version, sketch_id_point, constraint_point)
12775 .await;
12776 assert!(result_point.is_err(), "Single point should error for radius");
12777
12778 let initial_source_line = "\
12780sketch(on = XY) {
12781 line(start = [var 1, var 2], end = [var 3, var 4])
12782}
12783";
12784 let program_line = Program::parse(initial_source_line).unwrap().0.unwrap();
12785 let mut frontend_line = FrontendState::new();
12786 frontend_line.hack_set_program(&ctx, program_line).await.unwrap();
12787 let sketch_object_line = find_first_sketch_object(&frontend_line.scene_graph).unwrap();
12788 let sketch_id_line = sketch_object_line.id;
12789 let sketch_line = expect_sketch(sketch_object_line);
12790 let line_id = *sketch_line.segments.first().unwrap();
12791
12792 let constraint_line = Constraint::Radius(Radius {
12793 arc: line_id,
12794 radius: Number {
12795 value: 5.0,
12796 units: NumericSuffix::Mm,
12797 },
12798 label_position: None,
12799 source: Default::default(),
12800 });
12801 let result_line = frontend_line
12802 .add_constraint(&mock_ctx, version, sketch_id_line, constraint_line)
12803 .await;
12804 assert!(result_line.is_err(), "Single line segment should error for radius");
12805
12806 ctx.close().await;
12807 mock_ctx.close().await;
12808 }
12809
12810 #[tokio::test(flavor = "multi_thread")]
12811 async fn test_diameter_single_arc_segment() {
12812 let initial_source = "\
12813sketch(on = XY) {
12814 arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
12815}
12816";
12817
12818 let program = Program::parse(initial_source).unwrap().0.unwrap();
12819
12820 let mut frontend = FrontendState::new();
12821
12822 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
12823 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
12824 let version = Version(0);
12825
12826 frontend.hack_set_program(&ctx, program).await.unwrap();
12827 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12828 let sketch_id = sketch_object.id;
12829 let sketch = expect_sketch(sketch_object);
12830 let arc_id = sketch
12832 .segments
12833 .iter()
12834 .find(|&seg_id| {
12835 let obj = frontend.scene_graph.objects.get(seg_id.0);
12836 matches!(
12837 obj.map(|o| &o.kind),
12838 Some(ObjectKind::Segment {
12839 segment: Segment::Arc(_)
12840 })
12841 )
12842 })
12843 .unwrap();
12844
12845 let constraint = Constraint::Diameter(Diameter {
12846 arc: *arc_id,
12847 diameter: Number {
12848 value: 10.0,
12849 units: NumericSuffix::Mm,
12850 },
12851 label_position: None,
12852 source: Default::default(),
12853 });
12854 let (src_delta, scene_delta) = frontend
12855 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12856 .await
12857 .unwrap();
12858 insta::assert_snapshot!("test_diameter_single_arc_segment", src_delta.text.as_str());
12859 assert_eq!(
12860 scene_delta.new_graph.objects.len(),
12861 7, "{:#?}",
12863 scene_delta.new_graph.objects
12864 );
12865
12866 ctx.close().await;
12867 mock_ctx.close().await;
12868 }
12869
12870 #[tokio::test(flavor = "multi_thread")]
12871 async fn test_diameter_single_arc_segment_with_label_position() {
12872 let initial_source = "\
12873sketch(on = XY) {
12874 arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
12875}
12876";
12877
12878 let program = Program::parse(initial_source).unwrap().0.unwrap();
12879 let mut frontend = FrontendState::new();
12880 let mock_ctx = ExecutorContext::new_mock(None).await;
12881 let version = Version(0);
12882
12883 frontend.program = program.clone();
12884 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12885 frontend.update_state_after_exec(outcome, true);
12886 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12887 let sketch_id = sketch_object.id;
12888 let sketch = expect_sketch(sketch_object);
12889 let arc_id = sketch
12890 .segments
12891 .iter()
12892 .find(|&seg_id| {
12893 let obj = frontend.scene_graph.objects.get(seg_id.0);
12894 matches!(
12895 obj.map(|o| &o.kind),
12896 Some(ObjectKind::Segment {
12897 segment: Segment::Arc(_)
12898 })
12899 )
12900 })
12901 .unwrap();
12902
12903 let label_position = Point2d {
12904 x: Number {
12905 value: 10.0,
12906 units: NumericSuffix::Mm,
12907 },
12908 y: Number {
12909 value: 11.0,
12910 units: NumericSuffix::Mm,
12911 },
12912 };
12913 let constraint = Constraint::Diameter(Diameter {
12914 arc: *arc_id,
12915 diameter: Number {
12916 value: 10.0,
12917 units: NumericSuffix::Mm,
12918 },
12919 label_position: Some(label_position.clone()),
12920 source: Default::default(),
12921 });
12922 let (src_delta, scene_delta) = frontend
12923 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12924 .await
12925 .unwrap();
12926 insta::assert_snapshot!(
12927 "test_diameter_single_arc_segment_with_label_position",
12928 src_delta.text.as_str()
12929 );
12930
12931 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
12932 let sketch = expect_sketch(sketch_object);
12933 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
12934 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12935 panic!("Expected constraint object");
12936 };
12937 let Constraint::Diameter(diameter) = constraint else {
12938 panic!("Expected diameter constraint");
12939 };
12940 assert_eq!(diameter.label_position, Some(label_position));
12941
12942 mock_ctx.close().await;
12943 }
12944
12945 #[tokio::test(flavor = "multi_thread")]
12946 async fn test_edit_diameter_constraint_label_position() {
12947 let initial_source = "\
12948sketch(on = XY) {
12949 arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
12950 diameter(arc1) == 10mm
12951}
12952";
12953
12954 let program = Program::parse(initial_source).unwrap().0.unwrap();
12955 let mut frontend = FrontendState::new();
12956 let mock_ctx = ExecutorContext::new_mock(None).await;
12957 let version = Version(0);
12958
12959 frontend.program = program.clone();
12960 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12961 frontend.update_state_after_exec(outcome, true);
12962 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12963 let sketch_id = sketch_object.id;
12964 let sketch = expect_sketch(sketch_object);
12965 let constraint_id = sketch.constraints[0];
12966 let label_position = Point2d {
12967 x: Number {
12968 value: 10.0,
12969 units: NumericSuffix::Mm,
12970 },
12971 y: Number {
12972 value: 11.0,
12973 units: NumericSuffix::Mm,
12974 },
12975 };
12976
12977 let (src_delta, scene_delta) = frontend
12978 .edit_distance_constraint_label_position(
12979 &mock_ctx,
12980 version,
12981 sketch_id,
12982 constraint_id,
12983 label_position.clone(),
12984 vec![],
12985 )
12986 .await
12987 .unwrap();
12988 insta::assert_snapshot!("test_edit_diameter_constraint_label_position", src_delta.text.as_str());
12989
12990 let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
12991 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12992 panic!("Expected constraint object");
12993 };
12994 let Constraint::Diameter(diameter) = constraint else {
12995 panic!("Expected diameter constraint");
12996 };
12997 assert_eq!(diameter.label_position, Some(label_position));
12998
12999 mock_ctx.close().await;
13000 }
13001
13002 #[tokio::test(flavor = "multi_thread")]
13003 async fn test_diameter_error_cases() {
13004 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13005 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
13006 let version = Version(0);
13007
13008 let initial_source_point = "\
13010sketch(on = XY) {
13011 point(at = [var 1, var 2])
13012}
13013";
13014 let program_point = Program::parse(initial_source_point).unwrap().0.unwrap();
13015 let mut frontend_point = FrontendState::new();
13016 frontend_point.hack_set_program(&ctx, program_point).await.unwrap();
13017 let sketch_object_point = find_first_sketch_object(&frontend_point.scene_graph).unwrap();
13018 let sketch_id_point = sketch_object_point.id;
13019 let sketch_point = expect_sketch(sketch_object_point);
13020 let point_id = *sketch_point.segments.first().unwrap();
13021
13022 let constraint_point = Constraint::Diameter(Diameter {
13023 arc: point_id,
13024 diameter: Number {
13025 value: 10.0,
13026 units: NumericSuffix::Mm,
13027 },
13028 label_position: None,
13029 source: Default::default(),
13030 });
13031 let result_point = frontend_point
13032 .add_constraint(&mock_ctx, version, sketch_id_point, constraint_point)
13033 .await;
13034 assert!(result_point.is_err(), "Single point should error for diameter");
13035
13036 let initial_source_line = "\
13038sketch(on = XY) {
13039 line(start = [var 1, var 2], end = [var 3, var 4])
13040}
13041";
13042 let program_line = Program::parse(initial_source_line).unwrap().0.unwrap();
13043 let mut frontend_line = FrontendState::new();
13044 frontend_line.hack_set_program(&ctx, program_line).await.unwrap();
13045 let sketch_object_line = find_first_sketch_object(&frontend_line.scene_graph).unwrap();
13046 let sketch_id_line = sketch_object_line.id;
13047 let sketch_line = expect_sketch(sketch_object_line);
13048 let line_id = *sketch_line.segments.first().unwrap();
13049
13050 let constraint_line = Constraint::Diameter(Diameter {
13051 arc: line_id,
13052 diameter: Number {
13053 value: 10.0,
13054 units: NumericSuffix::Mm,
13055 },
13056 label_position: None,
13057 source: Default::default(),
13058 });
13059 let result_line = frontend_line
13060 .add_constraint(&mock_ctx, version, sketch_id_line, constraint_line)
13061 .await;
13062 assert!(result_line.is_err(), "Single line segment should error for diameter");
13063
13064 ctx.close().await;
13065 mock_ctx.close().await;
13066 }
13067
13068 #[tokio::test(flavor = "multi_thread")]
13069 async fn test_line_horizontal() {
13070 let initial_source = "\
13071sketch(on = XY) {
13072 line(start = [var 1, var 2], end = [var 3, var 4])
13073}
13074";
13075
13076 let program = Program::parse(initial_source).unwrap().0.unwrap();
13077
13078 let mut frontend = FrontendState::new();
13079
13080 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13081 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
13082 let version = Version(0);
13083
13084 frontend.hack_set_program(&ctx, program).await.unwrap();
13085 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13086 let sketch_id = sketch_object.id;
13087 let sketch = expect_sketch(sketch_object);
13088 let line1_id = *sketch.segments.get(2).unwrap();
13089
13090 let constraint = Constraint::Horizontal(Horizontal::Line { line: line1_id });
13091 let (src_delta, scene_delta) = frontend
13092 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13093 .await
13094 .unwrap();
13095 insta::assert_snapshot!("test_line_horizontal", src_delta.text.as_str());
13096 assert_eq!(
13097 scene_delta.new_graph.objects.len(),
13098 6,
13099 "{:#?}",
13100 scene_delta.new_graph.objects
13101 );
13102
13103 ctx.close().await;
13104 mock_ctx.close().await;
13105 }
13106
13107 #[tokio::test(flavor = "multi_thread")]
13108 async fn test_control_point_spline_edge_horizontal() {
13109 let initial_source = "\
13110@settings(experimentalFeatures = allow)
13111splineSketch = sketch(on = XY) {
13112 controlPointSpline1 = controlPointSpline(points = [
13113 [var 0mm, var 0mm],
13114 [var 10mm, var 20mm],
13115 [var 20mm, var 0mm],
13116 ])
13117}
13118";
13119
13120 let program = Program::parse(initial_source).unwrap().0.unwrap();
13121
13122 let mut frontend = FrontendState::new();
13123
13124 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13125 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
13126 let version = Version(0);
13127
13128 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
13129 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13130 let sketch_id = sketch_object.id;
13131 let sketch = expect_sketch(sketch_object);
13132 let spline_id = sketch
13133 .segments
13134 .iter()
13135 .copied()
13136 .find(|seg_id| {
13137 matches!(
13138 &frontend.scene_graph.objects[seg_id.0].kind,
13139 ObjectKind::Segment {
13140 segment: Segment::ControlPointSpline(_)
13141 }
13142 )
13143 })
13144 .expect("Expected a control point spline segment in sketch");
13145 let edge_id = frontend
13146 .scene_graph
13147 .objects
13148 .iter()
13149 .find_map(|obj| match &obj.kind {
13150 ObjectKind::Segment {
13151 segment: Segment::Line(line),
13152 } if line.owner == Some(spline_id) => Some(obj.id),
13153 _ => None,
13154 })
13155 .expect("Expected an owned control-polygon edge");
13156
13157 let constraint = Constraint::Horizontal(Horizontal::Line { line: edge_id });
13158 let (src_delta, _) = frontend
13159 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13160 .await
13161 .unwrap();
13162 assert!(
13163 src_delta.text.contains("horizontal(controlPointSpline1.edges[0])"),
13164 "Expected horizontal constraint on spline edge, got: {}",
13165 src_delta.text
13166 );
13167
13168 ctx.close().await;
13169 mock_ctx.close().await;
13170 }
13171
13172 #[tokio::test(flavor = "multi_thread")]
13173 async fn test_control_point_spline_edge_angle() {
13174 let initial_source = "\
13175@settings(experimentalFeatures = allow)
13176splineSketch = sketch(on = XY) {
13177 controlPointSpline1 = controlPointSpline(points = [
13178 [var 0mm, var 0mm],
13179 [var 10mm, var 20mm],
13180 [var 20mm, var 0mm],
13181 ])
13182
13183 line1 = line(start = [var 40mm, var 0mm], end = [var 60mm, var 10mm])
13184}
13185";
13186
13187 let program = Program::parse(initial_source).unwrap().0.unwrap();
13188
13189 let mut frontend = FrontendState::new();
13190
13191 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13192 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
13193 let version = Version(0);
13194
13195 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
13196 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13197 let sketch_id = sketch_object.id;
13198 let sketch = expect_sketch(sketch_object);
13199 let spline_id = sketch
13200 .segments
13201 .iter()
13202 .copied()
13203 .find(|seg_id| {
13204 matches!(
13205 &frontend.scene_graph.objects[seg_id.0].kind,
13206 ObjectKind::Segment {
13207 segment: Segment::ControlPointSpline(_)
13208 }
13209 )
13210 })
13211 .expect("Expected a control point spline segment in sketch");
13212 let edge_id = frontend
13213 .scene_graph
13214 .objects
13215 .iter()
13216 .find_map(|obj| match &obj.kind {
13217 ObjectKind::Segment {
13218 segment: Segment::Line(line),
13219 } if line.owner == Some(spline_id) => Some(obj.id),
13220 _ => None,
13221 })
13222 .expect("Expected an owned control-polygon edge");
13223 let line1_id = frontend
13224 .scene_graph
13225 .objects
13226 .iter()
13227 .find_map(|obj| match &obj.kind {
13228 ObjectKind::Segment {
13229 segment: Segment::Line(line),
13230 } if line.owner.is_none() && obj.label == "line1" => Some(obj.id),
13231 _ => None,
13232 })
13233 .or_else(|| {
13234 sketch.segments.iter().copied().find(|seg_id| {
13235 matches!(
13236 &frontend.scene_graph.objects[seg_id.0].kind,
13237 ObjectKind::Segment {
13238 segment: Segment::Line(line),
13239 } if line.owner.is_none()
13240 )
13241 })
13242 })
13243 .expect("Expected a standalone line segment in sketch");
13244
13245 let constraint = Constraint::Angle(Angle {
13246 lines: vec![line1_id, edge_id],
13247 angle: Number {
13248 value: 30.0,
13249 units: NumericSuffix::Deg,
13250 },
13251 sector: None,
13252 inverse: None,
13253 label_position: None,
13254 source: Default::default(),
13255 });
13256 let (src_delta, _) = frontend
13257 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13258 .await
13259 .unwrap();
13260 assert!(
13261 src_delta
13262 .text
13263 .contains("angle([line1, controlPointSpline1.edges[0]]) == 30deg"),
13264 "Expected angle constraint on spline edge, got: {}",
13265 src_delta.text
13266 );
13267
13268 ctx.close().await;
13269 mock_ctx.close().await;
13270 }
13271
13272 #[tokio::test(flavor = "multi_thread")]
13273 async fn test_ui_scene_graph_hides_same_spline_coincident_constraints() {
13274 let initial_source = "\
13275@settings(experimentalFeatures = allow)
13276splineSketch = sketch(on = XY) {
13277 spline1 = controlPointSpline(points = [
13278 [var 0mm, var 0mm],
13279 [var 10mm, var 20mm],
13280 [var 20mm, var 0mm],
13281 ])
13282 line1 = line(start = [var 0mm, var 0mm], end = [var -10mm, var 0mm])
13283 coincident([spline1.controls[1], spline1.edges[0]])
13284 coincident([spline1.controls[0], line1])
13285}
13286";
13287
13288 let program = Program::parse(initial_source).unwrap().0.unwrap();
13289
13290 let mut frontend = FrontendState::new();
13291
13292 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13293 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
13294
13295 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
13296
13297 let ui_scene_graph = frontend.scene_graph_for_ui();
13298 let sketch_object = find_first_sketch_object(&ui_scene_graph).unwrap();
13299 let sketch = expect_sketch(sketch_object);
13300
13301 assert_eq!(
13302 sketch.constraints.len(),
13303 1,
13304 "Expected only the external coincident constraint to remain visible in the UI scene graph"
13305 );
13306
13307 let visible_constraints = ui_scene_graph
13308 .objects
13309 .iter()
13310 .filter_map(|object| match &object.kind {
13311 ObjectKind::Constraint {
13312 constraint: Constraint::Coincident(coincident),
13313 } => Some(coincident.clone()),
13314 _ => None,
13315 })
13316 .collect::<Vec<_>>();
13317
13318 assert_eq!(
13319 visible_constraints.len(),
13320 1,
13321 "Expected only one coincident constraint object in the UI scene graph"
13322 );
13323 assert_eq!(
13324 visible_constraints[0].get_segments().len(),
13325 2,
13326 "Expected the remaining visible coincident constraint to reference two segments"
13327 );
13328
13329 ctx.close().await;
13330 mock_ctx.close().await;
13331 }
13332
13333 #[tokio::test(flavor = "multi_thread")]
13334 async fn test_edit_control_point_spline_can_append_control_point() {
13335 let initial_source = "\
13336@settings(experimentalFeatures = allow)
13337splineSketch = sketch(on = XY) {
13338 controlPointSpline(points = [
13339 [var 0mm, var 0mm],
13340 [var 10mm, var 20mm],
13341 [var 20mm, var 0mm],
13342 ])
13343}
13344";
13345
13346 let program = Program::parse(initial_source).unwrap().0.unwrap();
13347
13348 let mut frontend = FrontendState::new();
13349
13350 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13351 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
13352 let version = Version(0);
13353
13354 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
13355 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13356 let sketch_id = sketch_object.id;
13357 let sketch = expect_sketch(sketch_object);
13358 let spline_id = sketch
13359 .segments
13360 .iter()
13361 .copied()
13362 .find(|seg_id| {
13363 matches!(
13364 &frontend.scene_graph.objects[seg_id.0].kind,
13365 ObjectKind::Segment {
13366 segment: Segment::ControlPointSpline(_)
13367 }
13368 )
13369 })
13370 .expect("Expected a control point spline segment in sketch");
13371
13372 let ctor = ControlPointSplineCtor {
13373 points: vec![
13374 Point2d {
13375 x: Expr::Var(Number {
13376 value: 0.0,
13377 units: NumericSuffix::Mm,
13378 }),
13379 y: Expr::Var(Number {
13380 value: 0.0,
13381 units: NumericSuffix::Mm,
13382 }),
13383 },
13384 Point2d {
13385 x: Expr::Var(Number {
13386 value: 10.0,
13387 units: NumericSuffix::Mm,
13388 }),
13389 y: Expr::Var(Number {
13390 value: 20.0,
13391 units: NumericSuffix::Mm,
13392 }),
13393 },
13394 Point2d {
13395 x: Expr::Var(Number {
13396 value: 20.0,
13397 units: NumericSuffix::Mm,
13398 }),
13399 y: Expr::Var(Number {
13400 value: 0.0,
13401 units: NumericSuffix::Mm,
13402 }),
13403 },
13404 Point2d {
13405 x: Expr::Var(Number {
13406 value: 30.0,
13407 units: NumericSuffix::Mm,
13408 }),
13409 y: Expr::Var(Number {
13410 value: 10.0,
13411 units: NumericSuffix::Mm,
13412 }),
13413 },
13414 ],
13415 construction: None,
13416 };
13417
13418 let segments = vec![ExistingSegmentCtor {
13419 id: spline_id,
13420 ctor: SegmentCtor::ControlPointSpline(ctor),
13421 }];
13422 let (src_delta, scene_delta) = frontend
13423 .edit_segments(&mock_ctx, version, sketch_id, segments)
13424 .await
13425 .unwrap();
13426
13427 assert!(
13428 src_delta.text.contains("[var 30mm, var 10mm]"),
13429 "Expected appended spline control point in source, got: {}",
13430 src_delta.text
13431 );
13432
13433 assert!(
13434 scene_delta.invalidates_ids,
13435 "Expected appending a spline control point to invalidate ids"
13436 );
13437 let updated_spline = scene_delta
13438 .new_graph
13439 .objects
13440 .iter()
13441 .find_map(|obj| match &obj.kind {
13442 ObjectKind::Segment {
13443 segment: Segment::ControlPointSpline(updated_spline),
13444 } if updated_spline.controls.len() == 4 => Some(updated_spline),
13445 _ => None,
13446 })
13447 .expect("Expected edited scene graph to contain a four-point control point spline");
13448 assert_eq!(
13449 updated_spline.controls.len(),
13450 4,
13451 "Expected edited spline to expose four control points"
13452 );
13453
13454 ctx.close().await;
13455 mock_ctx.close().await;
13456 }
13457
13458 #[tokio::test(flavor = "multi_thread")]
13459 async fn test_line_vertical() {
13460 let initial_source = "\
13461sketch(on = XY) {
13462 line(start = [var 1, var 2], end = [var 3, var 4])
13463}
13464";
13465
13466 let program = Program::parse(initial_source).unwrap().0.unwrap();
13467
13468 let mut frontend = FrontendState::new();
13469
13470 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13471 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
13472 let version = Version(0);
13473
13474 frontend.hack_set_program(&ctx, program).await.unwrap();
13475 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13476 let sketch_id = sketch_object.id;
13477 let sketch = expect_sketch(sketch_object);
13478 let line1_id = *sketch.segments.get(2).unwrap();
13479
13480 let constraint = Constraint::Vertical(Vertical::Line { line: line1_id });
13481 let (src_delta, scene_delta) = frontend
13482 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13483 .await
13484 .unwrap();
13485 insta::assert_snapshot!("test_line_vertical", src_delta.text.as_str());
13486 assert_eq!(
13487 scene_delta.new_graph.objects.len(),
13488 6,
13489 "{:#?}",
13490 scene_delta.new_graph.objects
13491 );
13492
13493 ctx.close().await;
13494 mock_ctx.close().await;
13495 }
13496
13497 #[tokio::test(flavor = "multi_thread")]
13498 async fn test_points_vertical() {
13499 let initial_source = "\
13500sketch001 = sketch(on = XY) {
13501 p0 = point(at = [var -2.23mm, var 3.1mm])
13502 pf = point(at = [4, 4])
13503}
13504";
13505
13506 let program = Program::parse(initial_source).unwrap().0.unwrap();
13507
13508 let mut frontend = FrontendState::new();
13509
13510 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13511 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
13512 let version = Version(0);
13513
13514 frontend.hack_set_program(&ctx, program).await.unwrap();
13515 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13516 let sketch_id = sketch_object.id;
13517 let sketch = expect_sketch(sketch_object);
13518 let point_ids = vec![
13519 sketch.segments.first().unwrap().to_owned(),
13520 sketch.segments.get(1).unwrap().to_owned(),
13521 ];
13522
13523 let constraint = Constraint::Vertical(Vertical::Points {
13524 points: point_ids.into_iter().map(ConstraintSegment::from).collect(),
13525 });
13526 let (src_delta, scene_delta) = frontend
13527 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13528 .await
13529 .unwrap();
13530 insta::assert_snapshot!("test_points_vertical", src_delta.text.as_str());
13531 assert_eq!(
13532 scene_delta.new_graph.objects.len(),
13533 5,
13534 "{:#?}",
13535 scene_delta.new_graph.objects
13536 );
13537
13538 ctx.close().await;
13539 mock_ctx.close().await;
13540 }
13541
13542 #[tokio::test(flavor = "multi_thread")]
13543 async fn test_points_horizontal() {
13544 let initial_source = "\
13545sketch001 = sketch(on = XY) {
13546 p0 = point(at = [var -2.23mm, var 3.1mm])
13547 pf = point(at = [4, 4])
13548}
13549";
13550
13551 let program = Program::parse(initial_source).unwrap().0.unwrap();
13552
13553 let mut frontend = FrontendState::new();
13554
13555 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13556 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
13557 let version = Version(0);
13558
13559 frontend.hack_set_program(&ctx, program).await.unwrap();
13560 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13561 let sketch_id = sketch_object.id;
13562 let sketch = expect_sketch(sketch_object);
13563 let point_ids = vec![
13564 sketch.segments.first().unwrap().to_owned(),
13565 sketch.segments.get(1).unwrap().to_owned(),
13566 ];
13567
13568 let constraint = Constraint::Horizontal(Horizontal::Points {
13569 points: point_ids.into_iter().map(ConstraintSegment::from).collect(),
13570 });
13571 let (src_delta, scene_delta) = frontend
13572 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13573 .await
13574 .unwrap();
13575 insta::assert_snapshot!("test_points_horizontal", src_delta.text.as_str());
13576 assert_eq!(
13577 scene_delta.new_graph.objects.len(),
13578 5,
13579 "{:#?}",
13580 scene_delta.new_graph.objects
13581 );
13582
13583 ctx.close().await;
13584 mock_ctx.close().await;
13585 }
13586
13587 #[tokio::test(flavor = "multi_thread")]
13588 async fn test_point_horizontal_with_origin() {
13589 let initial_source = "\
13590sketch001 = sketch(on = XY) {
13591 p0 = point(at = [var -2.23mm, var 3.1mm])
13592}
13593";
13594
13595 let program = Program::parse(initial_source).unwrap().0.unwrap();
13596
13597 let mut frontend = FrontendState::new();
13598
13599 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13600 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
13601 let version = Version(0);
13602
13603 frontend.hack_set_program(&ctx, program).await.unwrap();
13604 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13605 let sketch_id = sketch_object.id;
13606 let sketch = expect_sketch(sketch_object);
13607 let point_id = *sketch.segments.first().unwrap();
13608
13609 let constraint = Constraint::Horizontal(Horizontal::Points {
13610 points: vec![ConstraintSegment::from(point_id), ConstraintSegment::ORIGIN],
13611 });
13612 let (src_delta, scene_delta) = frontend
13613 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13614 .await
13615 .unwrap();
13616 insta::assert_snapshot!("test_point_horizontal_with_origin", src_delta.text.as_str());
13617 assert_eq!(
13618 scene_delta.new_graph.objects.len(),
13619 4,
13620 "{:#?}",
13621 scene_delta.new_graph.objects
13622 );
13623
13624 ctx.close().await;
13625 mock_ctx.close().await;
13626 }
13627
13628 #[tokio::test(flavor = "multi_thread")]
13629 async fn test_lines_equal_length() {
13630 let initial_source = "\
13631sketch(on = XY) {
13632 line(start = [var 1, var 2], end = [var 3, var 4])
13633 line(start = [var 5, var 6], end = [var 7, var 8])
13634}
13635";
13636
13637 let program = Program::parse(initial_source).unwrap().0.unwrap();
13638
13639 let mut frontend = FrontendState::new();
13640
13641 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13642 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
13643 let version = Version(0);
13644
13645 frontend.hack_set_program(&ctx, program).await.unwrap();
13646 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13647 let sketch_id = sketch_object.id;
13648 let sketch = expect_sketch(sketch_object);
13649 let line1_id = *sketch.segments.get(2).unwrap();
13650 let line2_id = *sketch.segments.get(5).unwrap();
13651
13652 let constraint = Constraint::LinesEqualLength(LinesEqualLength {
13653 lines: vec![line1_id, line2_id],
13654 });
13655 let (src_delta, scene_delta) = frontend
13656 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13657 .await
13658 .unwrap();
13659 insta::assert_snapshot!("test_lines_equal_length", src_delta.text.as_str());
13660 assert_eq!(
13661 scene_delta.new_graph.objects.len(),
13662 9,
13663 "{:#?}",
13664 scene_delta.new_graph.objects
13665 );
13666
13667 ctx.close().await;
13668 mock_ctx.close().await;
13669 }
13670
13671 #[tokio::test(flavor = "multi_thread")]
13672 async fn test_add_constraint_multi_line_equal_length() {
13673 let initial_source = "\
13674sketch(on = XY) {
13675 line(start = [var 1, var 2], end = [var 3, var 4])
13676 line(start = [var 5, var 6], end = [var 7, var 8])
13677 line(start = [var 9, var 10], end = [var 11, var 12])
13678}
13679";
13680
13681 let program = Program::parse(initial_source).unwrap().0.unwrap();
13682
13683 let mut frontend = FrontendState::new();
13684 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13685 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
13686 let version = Version(0);
13687
13688 frontend.hack_set_program(&ctx, program).await.unwrap();
13689 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13690 let sketch_id = sketch_object.id;
13691 let sketch = expect_sketch(sketch_object);
13692 let line1_id = *sketch.segments.get(2).unwrap();
13693 let line2_id = *sketch.segments.get(5).unwrap();
13694 let line3_id = *sketch.segments.get(8).unwrap();
13695
13696 let constraint = Constraint::LinesEqualLength(LinesEqualLength {
13697 lines: vec![line1_id, line2_id, line3_id],
13698 });
13699 let (src_delta, scene_delta) = frontend
13700 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13701 .await
13702 .unwrap();
13703 insta::assert_snapshot!("test_add_constraint_multi_line_equal_length", src_delta.text.as_str());
13704 let constraints = scene_delta
13705 .new_graph
13706 .objects
13707 .iter()
13708 .filter_map(|obj| {
13709 let ObjectKind::Constraint { constraint } = &obj.kind else {
13710 return None;
13711 };
13712 Some(constraint)
13713 })
13714 .collect::<Vec<_>>();
13715
13716 assert_eq!(constraints.len(), 1, "{:#?}", frontend.scene_graph.objects);
13717 let Constraint::LinesEqualLength(lines_equal_length) = constraints[0] else {
13718 panic!("expected equal length constraint, got {:?}", constraints[0]);
13719 };
13720 assert_eq!(lines_equal_length.lines.len(), 3);
13721
13722 ctx.close().await;
13723 mock_ctx.close().await;
13724 }
13725
13726 #[tokio::test(flavor = "multi_thread")]
13727 async fn test_lines_parallel() {
13728 let initial_source = "\
13729sketch(on = XY) {
13730 line(start = [var 1, var 2], end = [var 3, var 4])
13731 line(start = [var 5, var 6], end = [var 7, var 8])
13732}
13733";
13734
13735 let program = Program::parse(initial_source).unwrap().0.unwrap();
13736
13737 let mut frontend = FrontendState::new();
13738
13739 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13740 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
13741 let version = Version(0);
13742
13743 frontend.hack_set_program(&ctx, program).await.unwrap();
13744 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13745 let sketch_id = sketch_object.id;
13746 let sketch = expect_sketch(sketch_object);
13747 let line1_id = *sketch.segments.get(2).unwrap();
13748 let line2_id = *sketch.segments.get(5).unwrap();
13749
13750 let constraint = Constraint::Parallel(Parallel {
13751 lines: vec![line1_id, line2_id],
13752 });
13753 let (src_delta, scene_delta) = frontend
13754 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13755 .await
13756 .unwrap();
13757 insta::assert_snapshot!("test_lines_parallel", src_delta.text.as_str());
13758 assert_eq!(
13759 scene_delta.new_graph.objects.len(),
13760 9,
13761 "{:#?}",
13762 scene_delta.new_graph.objects
13763 );
13764
13765 ctx.close().await;
13766 mock_ctx.close().await;
13767 }
13768
13769 #[tokio::test(flavor = "multi_thread")]
13770 async fn test_lines_parallel_multiline() {
13771 let initial_source = "\
13772sketch(on = XY) {
13773 line(start = [var 1, var 2], end = [var 3, var 4])
13774 line(start = [var 5, var 6], end = [var 7, var 8])
13775 line(start = [var 9, var 10], end = [var 11, var 12])
13776}
13777";
13778
13779 let program = Program::parse(initial_source).unwrap().0.unwrap();
13780
13781 let mut frontend = FrontendState::new();
13782
13783 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13784 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
13785 let version = Version(0);
13786
13787 frontend.hack_set_program(&ctx, program).await.unwrap();
13788 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13789 let sketch_id = sketch_object.id;
13790 let sketch = expect_sketch(sketch_object);
13791 let line1_id = *sketch.segments.get(2).unwrap();
13792 let line2_id = *sketch.segments.get(5).unwrap();
13793 let line3_id = *sketch.segments.get(8).unwrap();
13794
13795 let constraint = Constraint::Parallel(Parallel {
13796 lines: vec![line1_id, line2_id, line3_id],
13797 });
13798 let (src_delta, scene_delta) = frontend
13799 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13800 .await
13801 .unwrap();
13802 insta::assert_snapshot!("test_lines_parallel_multiline", src_delta.text.as_str());
13803
13804 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
13805 let sketch = expect_sketch(sketch_object);
13806 assert_eq!(sketch.constraints.len(), 1);
13807
13808 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
13809 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
13810 panic!("Expected constraint object");
13811 };
13812 let Constraint::Parallel(parallel) = constraint else {
13813 panic!("Expected parallel constraint");
13814 };
13815 assert_eq!(parallel.lines.len(), 3);
13816
13817 ctx.close().await;
13818 mock_ctx.close().await;
13819 }
13820
13821 #[tokio::test(flavor = "multi_thread")]
13822 async fn test_lines_perpendicular() {
13823 let initial_source = "\
13824sketch(on = XY) {
13825 line(start = [var 1, var 2], end = [var 3, var 4])
13826 line(start = [var 5, var 6], end = [var 7, var 8])
13827}
13828";
13829
13830 let program = Program::parse(initial_source).unwrap().0.unwrap();
13831
13832 let mut frontend = FrontendState::new();
13833
13834 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13835 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
13836 let version = Version(0);
13837
13838 frontend.hack_set_program(&ctx, program).await.unwrap();
13839 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13840 let sketch_id = sketch_object.id;
13841 let sketch = expect_sketch(sketch_object);
13842 let line1_id = *sketch.segments.get(2).unwrap();
13843 let line2_id = *sketch.segments.get(5).unwrap();
13844
13845 let constraint = Constraint::Perpendicular(Perpendicular {
13846 lines: vec![line1_id, line2_id],
13847 });
13848 let (src_delta, scene_delta) = frontend
13849 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13850 .await
13851 .unwrap();
13852 insta::assert_snapshot!("test_lines_perpendicular", src_delta.text.as_str());
13853 assert_eq!(
13854 scene_delta.new_graph.objects.len(),
13855 9,
13856 "{:#?}",
13857 scene_delta.new_graph.objects
13858 );
13859
13860 ctx.close().await;
13861 mock_ctx.close().await;
13862 }
13863
13864 #[tokio::test(flavor = "multi_thread")]
13865 async fn test_lines_angle() {
13866 let initial_source = "\
13867sketch(on = XY) {
13868 line(start = [var 1, var 2], end = [var 3, var 4])
13869 line(start = [var 5, var 6], end = [var 7, var 8])
13870}
13871";
13872
13873 let program = Program::parse(initial_source).unwrap().0.unwrap();
13874
13875 let mut frontend = FrontendState::new();
13876
13877 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13878 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
13879 let version = Version(0);
13880
13881 frontend.hack_set_program(&ctx, program).await.unwrap();
13882 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13883 let sketch_id = sketch_object.id;
13884 let sketch = expect_sketch(sketch_object);
13885 let line1_id = *sketch.segments.get(2).unwrap();
13886 let line2_id = *sketch.segments.get(5).unwrap();
13887
13888 let constraint = Constraint::Angle(Angle {
13889 lines: vec![line1_id, line2_id],
13890 angle: Number {
13891 value: 30.0,
13892 units: NumericSuffix::Deg,
13893 },
13894 sector: None,
13895 inverse: None,
13896 label_position: None,
13897 source: Default::default(),
13898 });
13899 let (src_delta, scene_delta) = frontend
13900 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13901 .await
13902 .unwrap();
13903 insta::assert_snapshot!("test_lines_angle", src_delta.text.as_str());
13904 assert_eq!(
13905 scene_delta.new_graph.objects.len(),
13906 9,
13907 "{:#?}",
13908 scene_delta.new_graph.objects
13909 );
13910
13911 ctx.close().await;
13912 mock_ctx.close().await;
13913 }
13914
13915 #[tokio::test(flavor = "multi_thread")]
13916 async fn test_lines_angle_with_sector_uses_angle_dimension() {
13917 let initial_source = "\
13918sketch(on = XY) {
13919 line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
13920 line(start = [var 0mm, var 0mm], end = [var 0mm, var 4mm])
13921}
13922";
13923
13924 let program = Program::parse(initial_source).unwrap().0.unwrap();
13925
13926 let mut frontend = FrontendState::new();
13927
13928 let mock_ctx = ExecutorContext::new_mock(None).await;
13929 let version = Version(0);
13930
13931 frontend.program = program.clone();
13932 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
13933 frontend.update_state_after_exec(outcome, true);
13934 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13935 let sketch_id = sketch_object.id;
13936 let sketch = expect_sketch(sketch_object);
13937 let line1_id = *sketch.segments.get(2).unwrap();
13938 let line2_id = *sketch.segments.get(5).unwrap();
13939
13940 let constraint = Constraint::Angle(Angle {
13941 lines: vec![line1_id, line2_id],
13942 angle: Number {
13943 value: 270.0,
13944 units: NumericSuffix::Deg,
13945 },
13946 sector: Some(1),
13947 inverse: Some(true),
13948 label_position: Some(Point2d {
13949 x: Number {
13950 value: -0.73,
13951 units: NumericSuffix::Mm,
13952 },
13953 y: Number {
13954 value: 0.75,
13955 units: NumericSuffix::Mm,
13956 },
13957 }),
13958 source: Default::default(),
13959 });
13960 let (src_delta, _) = frontend
13961 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13962 .await
13963 .unwrap();
13964 assert_eq!(
13965 src_delta.text.as_str(),
13966 "\
13967sketch(on = XY) {
13968 line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
13969 line2 = line(start = [var 0mm, var 0mm], end = [var 0mm, var 4mm])
13970 angleDimension(
13971 lines = [line1, line2],
13972 sector = 1,
13973 inverse = true,
13974 labelPosition = [-0.73mm, 0.75mm],
13975 ) == 270deg
13976}
13977"
13978 );
13979
13980 mock_ctx.close().await;
13981 }
13982
13983 #[tokio::test(flavor = "multi_thread")]
13984 async fn test_segments_tangent() {
13985 let initial_source = "\
13986sketch(on = XY) {
13987 line(start = [var 1, var 2], end = [var 3, var 4])
13988 arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
13989}
13990";
13991
13992 let program = Program::parse(initial_source).unwrap().0.unwrap();
13993
13994 let mut frontend = FrontendState::new();
13995
13996 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13997 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
13998 let version = Version(0);
13999
14000 frontend.hack_set_program(&ctx, program).await.unwrap();
14001 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14002 let sketch_id = sketch_object.id;
14003 let sketch = expect_sketch(sketch_object);
14004 let line1_id = *sketch.segments.get(2).unwrap();
14005 let arc1_id = *sketch.segments.get(6).unwrap();
14006
14007 let constraint = Constraint::Tangent(Tangent {
14008 input: vec![line1_id, arc1_id],
14009 });
14010 let (src_delta, scene_delta) = frontend
14011 .add_constraint(&mock_ctx, version, sketch_id, constraint)
14012 .await
14013 .unwrap();
14014 insta::assert_snapshot!("test_segments_tangent", src_delta.text.as_str());
14015 assert_eq!(
14016 scene_delta.new_graph.objects.len(),
14017 10,
14018 "{:#?}",
14019 scene_delta.new_graph.objects
14020 );
14021
14022 ctx.close().await;
14023 mock_ctx.close().await;
14024 }
14025
14026 #[tokio::test(flavor = "multi_thread")]
14027 async fn test_point_midpoint() {
14028 let initial_source = "\
14029sketch(on = XY) {
14030 point(at = [var 1, var 1])
14031 line(start = [var 0, var 0], end = [var 6, var 4])
14032}
14033";
14034
14035 let program = Program::parse(initial_source).unwrap().0.unwrap();
14036
14037 let mut frontend = FrontendState::new();
14038
14039 let ctx = ExecutorContext::new_mock(None).await;
14040 let version = Version(0);
14041
14042 frontend.program = program.clone();
14043 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14044 frontend.update_state_after_exec(outcome, true);
14045 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14046 let sketch_id = sketch_object.id;
14047 let sketch = expect_sketch(sketch_object);
14048 let point_id = *sketch.segments.first().unwrap();
14049 let line_id = *sketch.segments.get(3).unwrap();
14050
14051 let constraint = Constraint::Midpoint(Midpoint {
14052 point: ConstraintSegment::from(point_id),
14053 segment: line_id,
14054 });
14055 let (src_delta, scene_delta) = frontend
14056 .add_constraint(&ctx, version, sketch_id, constraint)
14057 .await
14058 .unwrap();
14059 insta::assert_snapshot!("test_point_midpoint", src_delta.text.as_str());
14060 assert_eq!(
14061 scene_delta.new_graph.objects.len(),
14062 7,
14063 "{:#?}",
14064 scene_delta.new_graph.objects
14065 );
14066
14067 ctx.close().await;
14068 }
14069
14070 #[tokio::test(flavor = "multi_thread")]
14071 async fn test_segments_symmetric() {
14072 let initial_source = "\
14073sketch(on = XY) {
14074 line(start = [var 0, var 0], end = [var 0, var 4])
14075 line(start = [var 4, var 0], end = [var 4, var 4])
14076 line(start = [var 2, var -1], end = [var 2, var 5])
14077}
14078";
14079
14080 let program = Program::parse(initial_source).unwrap().0.unwrap();
14081
14082 let mut frontend = FrontendState::new();
14083
14084 let ctx = ExecutorContext::new_mock(None).await;
14085 let version = Version(0);
14086
14087 frontend.program = program.clone();
14088 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14089 frontend.update_state_after_exec(outcome, true);
14090 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14091 let sketch_id = sketch_object.id;
14092 let sketch = expect_sketch(sketch_object);
14093 let line1_id = *sketch.segments.get(2).unwrap();
14094 let line2_id = *sketch.segments.get(5).unwrap();
14095 let axis_id = *sketch.segments.get(8).unwrap();
14096
14097 let constraint = Constraint::Symmetric(Symmetric {
14098 input: vec![line1_id, line2_id],
14099 axis: axis_id,
14100 });
14101 let (src_delta, scene_delta) = frontend
14102 .add_constraint(&ctx, version, sketch_id, constraint)
14103 .await
14104 .unwrap();
14105 insta::assert_snapshot!("test_segments_symmetric", src_delta.text.as_str());
14106 assert_eq!(
14107 scene_delta.new_graph.objects.len(),
14108 12,
14109 "{:#?}",
14110 scene_delta.new_graph.objects
14111 );
14112
14113 ctx.close().await;
14114 }
14115
14116 #[tokio::test(flavor = "multi_thread")]
14117 async fn test_point_arc_midpoint() {
14118 let initial_source = "\
14119sketch(on = XY) {
14120 point(at = [var 6, var 3])
14121 arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
14122}
14123";
14124
14125 let program = Program::parse(initial_source).unwrap().0.unwrap();
14126
14127 let mut frontend = FrontendState::new();
14128
14129 let ctx = ExecutorContext::new_mock(None).await;
14130 let version = Version(0);
14131
14132 frontend.program = program.clone();
14133 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14134 frontend.update_state_after_exec(outcome, true);
14135 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14136 let sketch_id = sketch_object.id;
14137 let sketch = expect_sketch(sketch_object);
14138 let point_id = *sketch.segments.first().unwrap();
14139 let arc_id = *sketch.segments.get(4).unwrap();
14140
14141 let constraint = Constraint::Midpoint(Midpoint {
14142 point: ConstraintSegment::from(point_id),
14143 segment: arc_id,
14144 });
14145 let (src_delta, scene_delta) = frontend
14146 .add_constraint(&ctx, version, sketch_id, constraint)
14147 .await
14148 .unwrap();
14149 insta::assert_snapshot!("test_point_arc_midpoint", src_delta.text.as_str());
14150 assert_eq!(
14151 scene_delta.new_graph.objects.len(),
14152 8,
14153 "{:#?}",
14154 scene_delta.new_graph.objects
14155 );
14156
14157 ctx.close().await;
14158 }
14159
14160 #[tokio::test(flavor = "multi_thread")]
14161 async fn test_origin_line_midpoint() {
14162 let initial_source = "\
14163sketch(on = XY) {
14164 line(start = [var 0, var 0], end = [var 6, var 4])
14165}
14166";
14167
14168 let program = Program::parse(initial_source).unwrap().0.unwrap();
14169
14170 let mut frontend = FrontendState::new();
14171
14172 let ctx = ExecutorContext::new_mock(None).await;
14173 let version = Version(0);
14174
14175 frontend.program = program.clone();
14176 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14177 frontend.update_state_after_exec(outcome, true);
14178 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14179 let sketch_id = sketch_object.id;
14180 let sketch = expect_sketch(sketch_object);
14181 let line_id = *sketch.segments.get(2).unwrap();
14182
14183 let constraint = Constraint::Midpoint(Midpoint {
14184 point: ConstraintSegment::ORIGIN,
14185 segment: line_id,
14186 });
14187 let (src_delta, scene_delta) = frontend
14188 .add_constraint(&ctx, version, sketch_id, constraint)
14189 .await
14190 .unwrap();
14191 insta::assert_snapshot!("test_origin_line_midpoint", src_delta.text.as_str());
14192 assert_eq!(
14193 scene_delta.new_graph.objects.len(),
14194 6,
14195 "{:#?}",
14196 scene_delta.new_graph.objects
14197 );
14198
14199 ctx.close().await;
14200 }
14201
14202 #[tokio::test(flavor = "multi_thread")]
14203 async fn test_origin_arc_midpoint() {
14204 let initial_source = "\
14205sketch(on = XY) {
14206 arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
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_mock(None).await;
14215 let version = Version(0);
14216
14217 frontend.program = program.clone();
14218 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14219 frontend.update_state_after_exec(outcome, true);
14220 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14221 let sketch_id = sketch_object.id;
14222 let sketch = expect_sketch(sketch_object);
14223 let arc_id = *sketch.segments.get(3).unwrap();
14224
14225 let constraint = Constraint::Midpoint(Midpoint {
14226 point: ConstraintSegment::ORIGIN,
14227 segment: arc_id,
14228 });
14229 let (src_delta, scene_delta) = frontend
14230 .add_constraint(&ctx, version, sketch_id, constraint)
14231 .await
14232 .unwrap();
14233 insta::assert_snapshot!("test_origin_arc_midpoint", src_delta.text.as_str());
14234 assert_eq!(
14235 scene_delta.new_graph.objects.len(),
14236 7,
14237 "{:#?}",
14238 scene_delta.new_graph.objects
14239 );
14240
14241 ctx.close().await;
14242 }
14243
14244 #[tokio::test(flavor = "multi_thread")]
14245 async fn test_segments_symmetric_arcs() {
14246 let initial_source = "\
14247sketch(on = XY) {
14248 arc(start = [var -15, var 0], end = [var -10, var 5], center = [var -10, var 0])
14249 arc(start = [var 6, var 2], end = [var 12, var -4], center = [var 8, var 1])
14250 line(start = [var 0, var -10], end = [var 0, var 10])
14251}
14252";
14253
14254 let program = Program::parse(initial_source).unwrap().0.unwrap();
14255
14256 let mut frontend = FrontendState::new();
14257
14258 let ctx = ExecutorContext::new_mock(None).await;
14259 let version = Version(0);
14260
14261 frontend.program = program.clone();
14262 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14263 frontend.update_state_after_exec(outcome, true);
14264 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14265 let sketch_id = sketch_object.id;
14266 let sketch = expect_sketch(sketch_object);
14267 let arc1_id = *sketch.segments.get(3).unwrap();
14268 let arc2_id = *sketch.segments.get(7).unwrap();
14269 let axis_id = *sketch.segments.get(10).unwrap();
14270
14271 let constraint = Constraint::Symmetric(Symmetric {
14272 input: vec![arc1_id, arc2_id],
14273 axis: axis_id,
14274 });
14275 let (src_delta, scene_delta) = frontend
14276 .add_constraint(&ctx, version, sketch_id, constraint)
14277 .await
14278 .unwrap();
14279 insta::assert_snapshot!("test_segments_symmetric_arcs", src_delta.text.as_str());
14280 assert_eq!(
14281 scene_delta.new_graph.objects.len(),
14282 14,
14283 "{:#?}",
14284 scene_delta.new_graph.objects
14285 );
14286
14287 ctx.close().await;
14288 }
14289
14290 #[tokio::test(flavor = "multi_thread")]
14291 async fn test_sketch_on_face_simple() {
14292 let initial_source = "\
14293len = 2mm
14294cube = startSketchOn(XY)
14295 |> startProfile(at = [0, 0])
14296 |> line(end = [len, 0], tag = $side)
14297 |> line(end = [0, len])
14298 |> line(end = [-len, 0])
14299 |> line(end = [0, -len])
14300 |> close()
14301 |> extrude(length = len)
14302
14303face = faceOf(cube, face = side)
14304";
14305
14306 let program = Program::parse(initial_source).unwrap().0.unwrap();
14307
14308 let mut frontend = FrontendState::new();
14309
14310 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
14311 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
14312 let version = Version(0);
14313
14314 frontend.hack_set_program(&ctx, program).await.unwrap();
14315 let face_object = find_first_face_object(&frontend.scene_graph).unwrap();
14316 let face_id = face_object.id;
14317
14318 let sketch_args = SketchCtor {
14319 on: Plane::Object(face_id),
14320 };
14321 let (_src_delta, scene_delta, sketch_id) = frontend
14322 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14323 .await
14324 .unwrap();
14325 assert_eq!(sketch_id, ObjectId(2));
14326 assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
14327 let sketch_object = &scene_delta.new_graph.objects[2];
14328 assert_eq!(sketch_object.id, ObjectId(2));
14329 assert_eq!(
14330 sketch_object.kind,
14331 ObjectKind::Sketch(Sketch {
14332 args: SketchCtor {
14333 on: Plane::Object(face_id),
14334 },
14335 plane: face_id,
14336 segments: vec![],
14337 constraints: vec![],
14338 })
14339 );
14340 assert_eq!(scene_delta.new_graph.objects.len(), 8);
14341
14342 ctx.close().await;
14343 mock_ctx.close().await;
14344 }
14345
14346 #[tokio::test(flavor = "multi_thread")]
14347 async fn test_new_sketch_on_primitive_index_face() {
14348 let initial_source = "\
14349@settings(kclVersion = 2.0)
14350
14351sketch001 = sketch(on = XY) {
14352 circle1 = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
14353}
14354extrude001 = extrude(region(point = [0mm, 0mm], sketch = sketch001), length = 5, tagEnd = $capEnd001)
14355shell001 = shell(extrude001, faces = capEnd001, thickness = 1)";
14356 let program = Program::parse(initial_source).unwrap().0.unwrap();
14357 let ctx = ExecutorContext::new_mock(None).await;
14358 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14359 let solid_id = match outcome.variables.get("shell001") {
14360 Some(KclValueView::Solid { value }) => value.id,
14361 value => panic!("expected shell001 to be a solid, got {value:?}"),
14362 };
14363 let solid_references = solid_references_from_variables(&program.ast, &outcome.variables);
14364
14365 let mut ast = program.ast;
14366 let scene_graph = SceneGraph::empty(ProjectId(0), FileId(0), Version(0));
14367 let face_expr = sketch_on_ast_expr(
14368 &mut ast,
14369 &scene_graph,
14370 &solid_references,
14371 &Plane::PrimitiveFace(kcl_api::PrimitiveFacePlane { solid_id, index: 6 }),
14372 )
14373 .unwrap();
14374 let face_decl = ast::VariableDeclaration::new(
14375 ast::VariableDeclarator::new("face001", face_expr),
14376 ast::ItemVisibility::Default,
14377 ast::VariableKind::Const,
14378 );
14379 ast.body
14380 .push(ast::BodyItem::VariableDeclaration(BoxNode::new(ast::Node::no_src(
14381 face_decl,
14382 ))));
14383 let face_source = source_from_ast(&ast);
14384 let new_source = format!("{face_source}sketch002 = sketch(on = face001) {{\n}}\n");
14385 insta::assert_snapshot!("test_new_sketch_on_primitive_index_face", new_source);
14386
14387 let program = Program::parse(&new_source).unwrap().0.unwrap();
14388 ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14389 ctx.close().await;
14390 }
14391
14392 #[tokio::test(flavor = "multi_thread")]
14393 async fn test_sketch_on_wall_artifact_from_region_extrude() {
14394 let initial_source = "\
14395s = sketch(on = YZ) {
14396 line1 = line(start = [0, 0], end = [0, 1])
14397 line2 = line(start = [0, 1], end = [1, 1])
14398 line3 = line(start = [1, 1], end = [0, 0])
14399}
14400region001 = region(point = [0.1, 0.1], sketch = s)
14401extrude001 = extrude(region001, length = 5)
14402";
14403
14404 let program = Program::parse(initial_source).unwrap().0.unwrap();
14405
14406 let mut frontend = FrontendState::new();
14407 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
14408 let version = Version(0);
14409
14410 frontend.hack_set_program(&ctx, program).await.unwrap();
14411 let wall_object_id = find_first_wall_object_id(&frontend.scene_graph).expect("expected a wall object");
14412
14413 let sketch_args = SketchCtor {
14414 on: Plane::Object(wall_object_id),
14415 };
14416 let (src_delta, _scene_delta, _sketch_id) = frontend
14417 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14418 .await
14419 .unwrap();
14420 assert!(src_delta.text.contains("faceOf(extrude001, face = region001.tags."));
14421
14422 ctx.close().await;
14423 }
14424
14425 #[tokio::test(flavor = "multi_thread")]
14426 async fn test_sketch_on_wall_artifact_from_split_region_extrude() {
14427 let initial_source = "\
14428sketch001 = sketch(on = YZ) {
14429 line1 = line(start = [var 0.49, var -0.39], end = [var 6.52, var -0.39])
14430 line2 = line(start = [var 6.52, var -0.39], end = [var 6.52, var 4.9])
14431 line3 = line(start = [var 6.52, var 4.9], end = [var 0.49, var 4.9])
14432 line4 = line(start = [var 0.49, var 4.9], end = [var 0.49, var -0.39])
14433 coincident([line1.end, line2.start])
14434 coincident([line2.end, line3.start])
14435 coincident([line3.end, line4.start])
14436 coincident([line4.end, line1.start])
14437 parallel([line2, line4])
14438 parallel([line3, line1])
14439 perpendicular([line1, line2])
14440 horizontal(line3)
14441 line5 = line(start = [2.35, 6.65], end = [5.89, -2.7])
14442}
14443region001 = region(point = [3.1, 3.74], sketch = sketch001)
14444extrude001 = extrude(region001, length = 5)
14445";
14446
14447 let program = Program::parse(initial_source).unwrap().0.unwrap();
14448
14449 let mut frontend = FrontendState::new();
14450 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
14451 let version = Version(0);
14452
14453 frontend.hack_set_program(&ctx, program).await.unwrap();
14454 let wall_object_id = find_first_wall_object_id(&frontend.scene_graph).expect("expected a wall object");
14455
14456 let sketch_args = SketchCtor {
14457 on: Plane::Object(wall_object_id),
14458 };
14459 let (src_delta, _scene_delta, _sketch_id) = frontend
14460 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14461 .await
14462 .unwrap();
14463 assert!(src_delta.text.contains("faceOf(extrude001, face = region001.tags."));
14464
14465 ctx.close().await;
14466 }
14467
14468 #[tokio::test(flavor = "multi_thread")]
14469 async fn test_new_sketch_on_multi_region_extrude_cap_indexes_selected_solid() {
14470 let initial_source = "\
14471@settings(kclVersion = 2.0)
14472
14473sketch001 = sketch(on = XY) {
14474 circle1 = circle(start = [var -1.51mm, var 1.31mm], center = [var -1.98mm, var 1.03mm])
14475 circle2 = circle(start = [var 2.98mm, var 2.37mm], center = [var 2.66mm, var 1.46mm])
14476}
14477hidden001 = hide(sketch001)
14478region001 = region(segments = [sketch001.circle2])
14479region002 = region(segments = [sketch001.circle1])
14480extrude001 = extrude([region001, region002], length = 5)
14481";
14482
14483 let program = Program::parse(initial_source).unwrap().0.unwrap();
14484 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
14485 let version = Version(0);
14486
14487 for (solid_output_index, expected_face) in [
14488 (0, "faceOf(extrude001[0], face = END)"),
14489 (1, "faceOf(extrude001[1], face = END)"),
14490 ] {
14491 let mut frontend = FrontendState::new();
14492 frontend.hack_set_program(&ctx, program.clone()).await.unwrap();
14493 let cap_object_id = find_cap_object_id_with_solid_output_index(
14494 &frontend.scene_graph,
14495 kcl_api::CapKind::End,
14496 solid_output_index,
14497 )
14498 .unwrap_or_else(|| panic!("expected an end cap object for solid output index {solid_output_index}"));
14499
14500 let sketch_args = SketchCtor {
14501 on: Plane::Object(cap_object_id),
14502 };
14503 let (src_delta, _scene_delta, _sketch_id) = frontend
14504 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14505 .await
14506 .unwrap();
14507
14508 assert!(
14509 src_delta.text.contains(expected_face),
14510 "expected `{expected_face}` in:\n{}",
14511 src_delta.text
14512 );
14513 assert!(!src_delta.text.contains("faceOf(extrude001, face = END)"));
14514 }
14515
14516 ctx.close().await;
14517 }
14518
14519 #[tokio::test(flavor = "multi_thread")]
14520 async fn test_new_sketch_on_multi_region_extrude_wall_indexes_selected_solid() {
14521 let initial_source = "\
14522@settings(kclVersion = 2.0)
14523
14524sketch001 = sketch(on = XY) {
14525 rect1Line1 = line(start = [0, 0], end = [1, 0])
14526 rect1Line2 = line(start = [1, 0], end = [1, 1])
14527 rect1Line3 = line(start = [1, 1], end = [0, 1])
14528 rect1Line4 = line(start = [0, 1], end = [0, 0])
14529 rect2Line1 = line(start = [3, 0], end = [4, 0])
14530 rect2Line2 = line(start = [4, 0], end = [4, 1])
14531 rect2Line3 = line(start = [4, 1], end = [3, 1])
14532 rect2Line4 = line(start = [3, 1], end = [3, 0])
14533}
14534hidden001 = hide(sketch001)
14535region001 = region(segments = [
14536 sketch001.rect1Line4,
14537 sketch001.rect1Line1
14538])
14539region002 = region(segments = [
14540 sketch001.rect2Line4,
14541 sketch001.rect2Line1
14542])
14543extrude001 = extrude([region001, region002], length = 5)
14544";
14545
14546 let program = Program::parse(initial_source).unwrap().0.unwrap();
14547 let mut frontend = FrontendState::new();
14548 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
14549 let version = Version(0);
14550
14551 frontend.hack_set_program(&ctx, program).await.unwrap();
14552 let region_call = "\
14553region(segments = [
14554 sketch001.rect1Line4,
14555 sketch001.rect1Line1
14556])";
14557 let region_call_start = initial_source.find(region_call).unwrap();
14558 let region_range = [region_call_start, region_call_start + region_call.len(), 0].into();
14559 let segment_call = "line(start = [0, 0], end = [1, 0])";
14560 let segment_call_start = initial_source.find(segment_call).unwrap();
14561 let segment_range = [segment_call_start, segment_call_start + segment_call.len(), 0].into();
14562 let wall_object_id = frontend
14563 .scene_graph
14564 .objects
14565 .iter()
14566 .find_map(|object| match &object.kind {
14567 ObjectKind::Wall(wall)
14568 if wall.source.path.as_ref().is_some_and(|path| path.range == region_range)
14569 && wall.source.segment.range == segment_range =>
14570 {
14571 Some(object.id)
14572 }
14573 _ => None,
14574 })
14575 .expect("expected a wall object for region001.tags.rect1Line1");
14576
14577 let sketch_args = SketchCtor {
14578 on: Plane::Object(wall_object_id),
14579 };
14580 let (src_delta, _scene_delta, _sketch_id) = frontend
14581 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14582 .await
14583 .unwrap();
14584
14585 let expected_face = "faceOf(extrude001[0], face = region001.tags.rect1Line1)";
14586 assert!(
14587 src_delta.text.contains(expected_face),
14588 "expected `{expected_face}` in:\n{}",
14589 src_delta.text
14590 );
14591 assert!(!src_delta.text.contains("faceOf(extrude001, face ="));
14592
14593 ctx.close().await;
14594 }
14595
14596 #[test]
14597 fn test_enclosing_variable_fallback_skips_nested_sketch_items() {
14598 let source = "\
14599sketch001 = sketch(on = XY) {
14600 line(start = [0, 0], end = [1, 0])
14601}
14602part = subtract(boxSolid, tools = [cutSolid])
14603 |> appearance(color = \"#8f96a3\")
14604";
14605 let ast = Program::parse(source).unwrap().0.unwrap().ast;
14606 let line_start = source.find("line").unwrap();
14607 let line_end = line_start + "line(start = [0, 0], end = [1, 0])".len();
14608 let line_ref = SourceRef::Simple {
14609 range: [line_start, line_end, 0].into(),
14610 node_path: None,
14611 };
14612 assert_eq!(variable_name_containing_source_ref(&ast, &line_ref), None);
14613
14614 let subtract_start = source.find("subtract").unwrap();
14615 let subtract_end = subtract_start + "subtract(boxSolid, tools = [cutSolid])".len();
14616 let subtract_ref = SourceRef::Simple {
14617 range: [subtract_start, subtract_end, 0].into(),
14618 node_path: None,
14619 };
14620 assert_eq!(
14621 variable_name_containing_source_ref(&ast, &subtract_ref),
14622 Some("part".to_owned())
14623 );
14624 }
14625
14626 #[tokio::test(flavor = "multi_thread")]
14627 async fn test_sketch_on_subtracted_sweep_cap_uses_composite_solid() {
14628 let source = "\
14629boxSolid = startSketchOn(XY)
14630 |> startProfile(at = [0, 0])
14631 |> line(end = [4, 0], tag = $bottomEdge)
14632 |> line(end = [0, 4])
14633 |> line(end = [-4, 0])
14634 |> close()
14635 |> extrude(length = 10)
14636cutSolid = startSketchOn(XY)
14637 |> startProfile(at = [1, 1])
14638 |> line(end = [1, 0])
14639 |> line(end = [0, 1])
14640 |> line(end = [-1, 0])
14641 |> close()
14642 |> extrude(length = 10)
14643part = subtract(boxSolid, tools = [cutSolid])
14644 |> appearance(color = \"#8f96a3\", roughness = 55, metalness = 8)
14645";
14646 let program = Program::parse(source).unwrap().0.unwrap();
14647 let mut frontend = FrontendState::new();
14648 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
14649 match frontend.hack_set_program(&ctx, program).await.unwrap() {
14650 SetProgramOutcome::Success { .. } => {}
14651 SetProgramOutcome::ExecFailure { error } => panic!("KCL fixture failed to execute: {error:?}"),
14652 }
14653
14654 let sweep_call_start = source.find("extrude").unwrap();
14655 let sweep_call_end = sweep_call_start + "extrude(length = 10)".len();
14656 let part_call_start = source.find("subtract").unwrap();
14657 let part_call_end = part_call_start + "subtract(boxSolid, tools = [cutSolid])".len();
14658 let sweep_range = [sweep_call_start, sweep_call_end, 0].into();
14659 let composite_range = [part_call_start, part_call_end, 0].into();
14660
14661 let cap_object = frontend
14662 .scene_graph
14663 .objects
14664 .iter()
14665 .find(|object| {
14666 matches!(
14667 &object.kind,
14668 ObjectKind::Cap(kcl_api::Cap {
14669 kind: kcl_api::CapKind::End,
14670 source,
14671 ..
14672 }) if source.solid.range == composite_range && source.sweep.range == sweep_range
14673 )
14674 })
14675 .expect("expected end cap object to trace through subtract and original extrude");
14676
14677 let mut ast = frontend.program.ast.clone();
14678 let cap_expr = sketch_on_ast_expr(
14679 &mut ast,
14680 &frontend.scene_graph,
14681 &frontend.solid_references,
14682 &Plane::Object(cap_object.id),
14683 )
14684 .unwrap();
14685 let cap_face_decl = ast::VariableDeclaration::new(
14686 ast::VariableDeclarator::new("capFace", cap_expr.clone()),
14687 ast::ItemVisibility::Default,
14688 ast::VariableKind::Const,
14689 );
14690 ast.body
14691 .push(ast::BodyItem::VariableDeclaration(BoxNode::new(ast::Node::no_src(
14692 cap_face_decl,
14693 ))));
14694 let generated_source = source_from_ast(&ast);
14695
14696 assert!(generated_source.contains("capFace = faceOf(part, face = END)"));
14697 assert!(!generated_source.contains("faceOf(boxSolid"));
14698 let ast::Expr::CallExpressionKw(call) = cap_expr else {
14699 panic!("expected faceOf call");
14700 };
14701 assert_eq!(call.callee.name.name, "faceOf");
14702 let ast::Expr::Name(solid_name) = call.unlabeled.as_ref().unwrap() else {
14703 panic!("expected solid name");
14704 };
14705 assert_eq!(solid_name.name.name, "part");
14706 let ast::Expr::Name(face_name) = &call.arguments[0].arg else {
14707 panic!("expected face name");
14708 };
14709 assert_eq!(face_name.name.name, "END");
14710
14711 ctx.close().await;
14712 }
14713
14714 #[tokio::test(flavor = "multi_thread")]
14715 async fn test_sketch_on_plane_incremental() {
14716 let initial_source = "\
14717len = 2mm
14718cube = startSketchOn(XY)
14719 |> startProfile(at = [0, 0])
14720 |> line(end = [len, 0], tag = $side)
14721 |> line(end = [0, len])
14722 |> line(end = [-len, 0])
14723 |> line(end = [0, -len])
14724 |> close()
14725 |> extrude(length = len)
14726
14727plane = planeOf(cube, face = side)
14728";
14729
14730 let program = Program::parse(initial_source).unwrap().0.unwrap();
14731
14732 let mut frontend = FrontendState::new();
14733
14734 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
14735 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
14736 let version = Version(0);
14737
14738 frontend.hack_set_program(&ctx, program).await.unwrap();
14739 let plane_object = frontend
14741 .scene_graph
14742 .objects
14743 .iter()
14744 .rev()
14745 .find(|object| matches!(&object.kind, ObjectKind::Plane(_)))
14746 .unwrap();
14747 let plane_id = plane_object.id;
14748
14749 let sketch_args = SketchCtor {
14750 on: Plane::Object(plane_id),
14751 };
14752 let (src_delta, scene_delta, sketch_id) = frontend
14753 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14754 .await
14755 .unwrap();
14756 insta::assert_snapshot!("test_sketch_on_plane_incremental", src_delta.text.as_str());
14757 assert_eq!(sketch_id, ObjectId(2));
14758 assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
14759 let sketch_object = &scene_delta.new_graph.objects[2];
14760 assert_eq!(sketch_object.id, ObjectId(2));
14761 assert_eq!(
14762 sketch_object.kind,
14763 ObjectKind::Sketch(Sketch {
14764 args: SketchCtor {
14765 on: Plane::Object(plane_id),
14766 },
14767 plane: plane_id,
14768 segments: vec![],
14769 constraints: vec![],
14770 })
14771 );
14772 assert_eq!(scene_delta.new_graph.objects.len(), 9);
14773
14774 let plane_object = scene_delta.new_graph.objects.get(plane_id.0).unwrap();
14775 assert_eq!(plane_object.id, plane_id);
14776 assert_eq!(plane_object.kind, ObjectKind::Plane(Plane::Object(plane_id)));
14777
14778 ctx.close().await;
14779 mock_ctx.close().await;
14780 }
14781
14782 #[tokio::test(flavor = "multi_thread")]
14783 async fn test_new_sketch_uses_unique_variable_name() {
14784 let initial_source = "\
14785sketch1 = sketch(on = XY) {
14786}
14787";
14788
14789 let program = Program::parse(initial_source).unwrap().0.unwrap();
14790
14791 let mut frontend = FrontendState::new();
14792 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
14793 let version = Version(0);
14794
14795 frontend.hack_set_program(&ctx, program).await.unwrap();
14796
14797 let sketch_args = SketchCtor {
14798 on: Plane::Default(PlaneName::Yz),
14799 };
14800 let (src_delta, _, _) = frontend
14801 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14802 .await
14803 .unwrap();
14804
14805 insta::assert_snapshot!("test_new_sketch_uses_unique_variable_name", src_delta.text.as_str());
14806
14807 ctx.close().await;
14808 }
14809
14810 #[tokio::test(flavor = "multi_thread")]
14811 async fn test_new_sketch_twice_using_same_plane() {
14812 let initial_source = "\
14813sketch1 = sketch(on = XY) {
14814}
14815";
14816
14817 let program = Program::parse(initial_source).unwrap().0.unwrap();
14818
14819 let mut frontend = FrontendState::new();
14820 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
14821 let version = Version(0);
14822
14823 frontend.hack_set_program(&ctx, program).await.unwrap();
14824
14825 let sketch_args = SketchCtor {
14826 on: Plane::Default(PlaneName::Xy),
14827 };
14828 let (src_delta, _, _) = frontend
14829 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14830 .await
14831 .unwrap();
14832
14833 insta::assert_snapshot!("test_new_sketch_twice_using_same_plane", src_delta.text.as_str());
14834
14835 ctx.close().await;
14836 }
14837
14838 #[tokio::test(flavor = "multi_thread")]
14839 async fn test_sketch_mode_reuses_cached_on_expression() {
14840 let initial_source = "\
14841width = 2mm
14842sketch(on = offsetPlane(XY, offset = width)) {
14843 line1 = line(start = [var 0, var 0], end = [var 1mm, var 0])
14844 distance([line1.start, line1.end]) == width
14845}
14846";
14847 let program = Program::parse(initial_source).unwrap().0.unwrap();
14848
14849 let mut frontend = FrontendState::new();
14850 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
14851 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
14852 let version = Version(0);
14853 let project_id = ProjectId(0);
14854 let file_id = FileId(0);
14855
14856 frontend.hack_set_program(&ctx, program).await.unwrap();
14857 let initial_object_count = frontend.scene_graph.objects.len();
14858 let sketch_id = find_first_sketch_object(&frontend.scene_graph)
14859 .expect("Expected sketch object to exist")
14860 .id;
14861
14862 let scene_delta = frontend
14865 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
14866 .await
14867 .unwrap();
14868 assert_eq!(scene_delta.new_graph.objects.len(), initial_object_count);
14869
14870 let (_src_delta, scene_delta) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
14873 assert_eq!(scene_delta.new_graph.objects.len(), initial_object_count);
14874
14875 ctx.close().await;
14876 mock_ctx.close().await;
14877 }
14878
14879 #[tokio::test(flavor = "multi_thread")]
14880 async fn test_edit_sketch_nested_in_pipe() {
14881 let source = r#"
14882profile = sketch(on = XY) {
14883 line1 = line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])
14884}
14885 |> translate(x = 2mm)
14886"#;
14887 let program = Program::parse_no_errs(source).unwrap();
14888 let mut frontend = FrontendState::new();
14889 let mock_ctx = ExecutorContext::new_mock(None).await;
14890 let version = Version(0);
14891
14892 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
14893 let sketch_id = find_first_sketch_object(&frontend.scene_graph)
14894 .expect("Expected piped sketch object")
14895 .id;
14896
14897 let scene_delta = frontend
14898 .edit_sketch(&mock_ctx, ProjectId(0), FileId(0), version, sketch_id)
14899 .await
14900 .unwrap();
14901 assert_eq!(scene_delta.new_graph.sketch_mode, Some(sketch_id));
14902 assert!(
14903 scene_delta
14904 .new_graph
14905 .objects
14906 .iter()
14907 .any(|object| matches!(&object.kind, ObjectKind::Segment { .. })),
14908 "Expected the piped sketch's segments to be present in sketch mode"
14909 );
14910
14911 mock_ctx.cache.clear_mem_cache().await;
14912 mock_ctx.close().await;
14913 }
14914
14915 #[tokio::test(flavor = "multi_thread")]
14916 async fn test_issue_9409_edit_sketch_nested_in_if_with_var_feedback() {
14917 let source = r#"
14918useFirstProfile = true
14919
14920profile = if useFirstProfile {
14921 sketch(on = XY) {
14922 line1 = line(start = [0mm, 0mm], end = [var 20mm, var 10mm])
14923 }
14924} else {
14925 sketch(on = XY) {
14926 line2 = line(start = [0mm, 0mm], end = [var 10mm, var 20mm])
14927 }
14928}
14929"#;
14930 let program = Program::parse_no_errs(source).unwrap();
14931 let mut frontend = FrontendState::new();
14932 let mock_ctx = ExecutorContext::new_mock(None).await;
14933 let version = Version(0);
14934
14935 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
14936 let sketch_object =
14937 find_first_sketch_object(&frontend.scene_graph).expect("Expected active branch's sketch object");
14938 let sketch_id = sketch_object.id;
14939 let sketch = expect_sketch(sketch_object);
14940 let line_end_id = *sketch
14941 .segments
14942 .get(1)
14943 .expect("Expected the active branch's line end point");
14944
14945 let scene_delta = frontend
14946 .edit_sketch(&mock_ctx, ProjectId(0), FileId(0), version, sketch_id)
14947 .await
14948 .unwrap();
14949 assert_eq!(scene_delta.new_graph.sketch_mode, Some(sketch_id));
14950
14951 let segments = vec![ExistingSegmentCtor {
14952 id: line_end_id,
14953 ctor: SegmentCtor::Point(PointCtor {
14954 position: Point2d {
14955 x: Expr::Var(Number {
14956 value: 30.0,
14957 units: NumericSuffix::Mm,
14958 }),
14959 y: Expr::Var(Number {
14960 value: 15.0,
14961 units: NumericSuffix::Mm,
14962 }),
14963 },
14964 }),
14965 }];
14966 let (source_delta, _) = frontend
14967 .edit_segments(&mock_ctx, version, sketch_id, segments)
14968 .await
14969 .unwrap();
14970 assert!(
14971 source_delta
14972 .text
14973 .contains("line1 = line(start = [0mm, 0mm], end = [var 30mm, var 15mm])"),
14974 "Expected the active branch's dragged variables to be updated:\n{}",
14975 source_delta.text
14976 );
14977 assert!(
14978 source_delta
14979 .text
14980 .contains("line2 = line(start = [0mm, 0mm], end = [var 10mm, var 20mm])"),
14981 "Expected the inactive branch to remain unchanged:\n{}",
14982 source_delta.text
14983 );
14984
14985 mock_ctx.cache.clear_mem_cache().await;
14986 mock_ctx.close().await;
14987 }
14988
14989 #[tokio::test(flavor = "multi_thread")]
14990 async fn test_multiple_sketch_blocks() {
14991 let initial_source = "\
14992// Cube that requires the engine.
14993width = 2
14994sketch001 = startSketchOn(XY)
14995profile001 = startProfile(sketch001, at = [0, 0])
14996 |> yLine(length = width, tag = $seg1)
14997 |> xLine(length = width)
14998 |> yLine(length = -width)
14999 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
15000 |> close()
15001extrude001 = extrude(profile001, length = width)
15002
15003// Get a value that requires the engine.
15004x = segLen(seg1)
15005
15006// Triangle with side length 2*x.
15007sketch(on = XY) {
15008 line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
15009 line2 = line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
15010 coincident([line1.end, line2.start])
15011 line3 = line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
15012 coincident([line2.end, line3.start])
15013 coincident([line3.end, line1.start])
15014 equalLength([line3, line1])
15015 equalLength([line1, line2])
15016 distance([line1.start, line1.end]) == 2*x
15017}
15018
15019// Line segment with length x.
15020sketch2 = sketch(on = XY) {
15021 line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
15022 distance([line1.start, line1.end]) == x
15023}
15024";
15025
15026 let program = Program::parse(initial_source).unwrap().0.unwrap();
15027
15028 let mut frontend = FrontendState::new();
15029
15030 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
15031 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
15032 let version = Version(0);
15033 let project_id = ProjectId(0);
15034 let file_id = FileId(0);
15035
15036 frontend.hack_set_program(&ctx, program).await.unwrap();
15037 let sketch_objects = frontend
15038 .scene_graph
15039 .objects
15040 .iter()
15041 .filter(|obj| matches!(obj.kind, ObjectKind::Sketch(_)))
15042 .collect::<Vec<_>>();
15043 let sketch1_id = sketch_objects.first().unwrap().id;
15044 let sketch2_id = sketch_objects.get(1).unwrap().id;
15045 let point1_id = ObjectId(sketch1_id.0 + 1);
15047 let point2_id = ObjectId(sketch2_id.0 + 1);
15049
15050 let scene_delta = frontend
15059 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch1_id)
15060 .await
15061 .unwrap();
15062 assert_eq!(
15063 scene_delta.new_graph.objects.len(),
15064 18,
15065 "{:#?}",
15066 scene_delta.new_graph.objects
15067 );
15068
15069 let point_ctor = PointCtor {
15071 position: Point2d {
15072 x: Expr::Var(Number {
15073 value: 1.0,
15074 units: NumericSuffix::Mm,
15075 }),
15076 y: Expr::Var(Number {
15077 value: 2.0,
15078 units: NumericSuffix::Mm,
15079 }),
15080 },
15081 };
15082 let segments = vec![ExistingSegmentCtor {
15083 id: point1_id,
15084 ctor: SegmentCtor::Point(point_ctor),
15085 }];
15086 let (src_delta, _) = frontend
15087 .edit_segments(&mock_ctx, version, sketch1_id, segments)
15088 .await
15089 .unwrap();
15090 insta::assert_snapshot!("test_multiple_sketch_blocks_1", src_delta.text.as_str());
15092 let edited_sketch1_source = src_delta.text.clone();
15093
15094 let (src_delta, _) = frontend.execute_mock(&mock_ctx, version, sketch1_id).await.unwrap();
15096 assert_eq!(src_delta.text, edited_sketch1_source);
15097 let scene = frontend.exit_sketch(&ctx, version, sketch1_id).await.unwrap();
15105 assert_eq!(scene.objects.len(), 30, "{:#?}", scene.objects);
15106
15107 let scene_delta = frontend
15115 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch2_id)
15116 .await
15117 .unwrap();
15118 assert_eq!(
15119 scene_delta.new_graph.objects.len(),
15120 24,
15121 "{:#?}",
15122 scene_delta.new_graph.objects
15123 );
15124
15125 let point_ctor = PointCtor {
15127 position: Point2d {
15128 x: Expr::Var(Number {
15129 value: 3.0,
15130 units: NumericSuffix::Mm,
15131 }),
15132 y: Expr::Var(Number {
15133 value: 4.0,
15134 units: NumericSuffix::Mm,
15135 }),
15136 },
15137 };
15138 let segments = vec![ExistingSegmentCtor {
15139 id: point2_id,
15140 ctor: SegmentCtor::Point(point_ctor),
15141 }];
15142 let (src_delta, _) = frontend
15143 .edit_segments(&mock_ctx, version, sketch2_id, segments)
15144 .await
15145 .unwrap();
15146 insta::assert_snapshot!("test_multiple_sketch_blocks_2", src_delta.text.as_str());
15148 let edited_sketch2_source = src_delta.text.clone();
15149
15150 let (src_delta, _) = frontend.execute_mock(&mock_ctx, version, sketch2_id).await.unwrap();
15152 assert_eq!(src_delta.text, edited_sketch2_source);
15153
15154 ctx.close().await;
15155 mock_ctx.close().await;
15156 }
15157
15158 #[tokio::test(flavor = "multi_thread")]
15159 async fn test_exit_sketch_without_changes_allows_entering_next_sketch() {
15160 let source = r#"sketch001 = sketch(on = XZ) {
15161 circle1 = circle(start = [var -1.96mm, var 2.77mm], center = [var -2.69mm, var 3.44mm])
15162}
15163sketch002 = sketch(on = XY) {
15164 line1 = line(start = [var 0mm, var 0mm], end = [var 4.68mm, var 0mm])
15165 line2 = line(start = [var 4.68mm, var 0mm], end = [var 4.68mm, var 2.96mm])
15166 line3 = line(start = [var 4.68mm, var 2.96mm], end = [var 0mm, var 2.96mm])
15167 line4 = line(start = [var 0mm, var 2.96mm], end = [var 0mm, var 0mm])
15168 coincident([line1.end, line2.start])
15169 coincident([line2.end, line3.start])
15170 coincident([line3.end, line4.start])
15171 coincident([line4.end, line1.start])
15172 parallel([line2, line4])
15173 parallel([line3, line1])
15174 perpendicular([line1, line2])
15175 horizontal(line3)
15176 coincident([line1.start, ORIGIN])
15177}
15178"#;
15179
15180 let program = Program::parse(source).unwrap().0.unwrap();
15181 let mut frontend = FrontendState::new();
15182 let ctx = ExecutorContext::new_with_engine(sync::Arc::new(EngineManager::new_mock()), Default::default());
15183 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
15184 let version = Version(0);
15185 let project_id = ProjectId(0);
15186 let file_id = FileId(0);
15187
15188 frontend.hack_set_program(&ctx, program).await.unwrap();
15189 let sketch_objects = frontend
15190 .scene_graph
15191 .objects
15192 .iter()
15193 .filter(|object| matches!(object.kind, ObjectKind::Sketch(_)))
15194 .collect::<Vec<_>>();
15195 assert_eq!(sketch_objects.len(), 2, "{:#?}", frontend.scene_graph.objects);
15196
15197 let sketch1_id = sketch_objects[0].id;
15198 let sketch2_id = sketch_objects[1].id;
15199
15200 frontend
15201 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch1_id)
15202 .await
15203 .unwrap();
15204 frontend.exit_sketch(&ctx, version, sketch1_id).await.unwrap();
15205
15206 let scene_delta = frontend
15207 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch2_id)
15208 .await
15209 .unwrap();
15210 assert_eq!(scene_delta.new_graph.sketch_mode, Some(sketch2_id));
15211
15212 ctx.cache.clear_mem_cache().await;
15213 ctx.close().await;
15214 mock_ctx.close().await;
15215 }
15216
15217 #[tokio::test(flavor = "multi_thread")]
15222 async fn test_extra_newlines_after_settings_edit_sketch_add_point() {
15223 let initial_source = "@settings(defaultLengthUnit = mm)
15225
15226sketch001 = sketch(on = XY) {
15227 point(at = [1in, 2in])
15228}
15229";
15230
15231 let program = Program::parse(initial_source).unwrap().0.unwrap();
15232 let mut frontend = FrontendState::new();
15233
15234 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
15235 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
15236 let version = Version(0);
15237 let project_id = ProjectId(0);
15238 let file_id = FileId(0);
15239
15240 frontend.hack_set_program(&ctx, program).await.unwrap();
15241 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15242 let sketch_id = sketch_object.id;
15243
15244 frontend
15246 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
15247 .await
15248 .unwrap();
15249
15250 let point_ctor = PointCtor {
15252 position: Point2d {
15253 x: Expr::Number(Number {
15254 value: 5.0,
15255 units: NumericSuffix::Mm,
15256 }),
15257 y: Expr::Number(Number {
15258 value: 6.0,
15259 units: NumericSuffix::Mm,
15260 }),
15261 },
15262 };
15263 let segment = SegmentCtor::Point(point_ctor);
15264 let (src_delta, scene_delta) = frontend
15265 .add_segment(&mock_ctx, version, sketch_id, segment, None)
15266 .await
15267 .unwrap();
15268 assert!(
15270 src_delta.text.contains("point(at = [5mm, 6mm])"),
15271 "Expected new point in source, got: {}",
15272 src_delta.text
15273 );
15274 assert!(!scene_delta.new_objects.is_empty());
15275
15276 ctx.close().await;
15277 mock_ctx.close().await;
15278 }
15279
15280 #[tokio::test(flavor = "multi_thread")]
15281 async fn test_ensure_control_point_spline_experimental_features_adds_allow_setting() {
15282 let initial_program = Program::parse("s = sketch(on = XY) {}\n").unwrap().0.unwrap();
15283
15284 let updated_program = ensure_control_point_spline_experimental_features(&initial_program).unwrap();
15285 let meta_settings = updated_program.meta_settings().unwrap().unwrap();
15286
15287 assert_eq!(meta_settings.experimental_features, WarningLevel::Allow);
15288 assert!(
15289 source_from_ast(&updated_program.ast).contains("@settings(experimentalFeatures = allow)"),
15290 "Expected experimental settings to be added to source"
15291 );
15292 }
15293
15294 #[tokio::test(flavor = "multi_thread")]
15295 async fn test_extra_newlines_after_settings_add_line_to_empty_sketch() {
15296 let initial_source = "@settings(defaultLengthUnit = mm)
15298
15299s = sketch(on = XY) {}
15300";
15301
15302 let program = Program::parse(initial_source).unwrap().0.unwrap();
15303 let mut frontend = FrontendState::new();
15304
15305 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
15306 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
15307 let version = Version(0);
15308
15309 frontend.hack_set_program(&ctx, program).await.unwrap();
15310 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15311 let sketch_id = sketch_object.id;
15312
15313 let line_ctor = LineCtor {
15314 start: Point2d {
15315 x: Expr::Number(Number {
15316 value: 0.0,
15317 units: NumericSuffix::Mm,
15318 }),
15319 y: Expr::Number(Number {
15320 value: 0.0,
15321 units: NumericSuffix::Mm,
15322 }),
15323 },
15324 end: Point2d {
15325 x: Expr::Number(Number {
15326 value: 10.0,
15327 units: NumericSuffix::Mm,
15328 }),
15329 y: Expr::Number(Number {
15330 value: 10.0,
15331 units: NumericSuffix::Mm,
15332 }),
15333 },
15334 construction: None,
15335 };
15336 let segment = SegmentCtor::Line(line_ctor);
15337 let (src_delta, scene_delta) = frontend
15338 .add_segment(&mock_ctx, version, sketch_id, segment, None)
15339 .await
15340 .unwrap();
15341 assert!(
15342 src_delta.text.contains("line(start = [0mm, 0mm], end = [10mm, 10mm])"),
15343 "Expected line in source, got: {}",
15344 src_delta.text
15345 );
15346 assert_eq!(scene_delta.new_objects.len(), 3);
15348
15349 ctx.close().await;
15350 mock_ctx.close().await;
15351 }
15352
15353 #[tokio::test(flavor = "multi_thread")]
15354 async fn test_extra_newlines_between_operations_edit_line() {
15355 let initial_source = "@settings(defaultLengthUnit = mm)
15357
15358sketch001 = sketch(on = XY) {
15359
15360 line1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm])
15361
15362}
15363";
15364
15365 let program = Program::parse(initial_source).unwrap().0.unwrap();
15366 let mut frontend = FrontendState::new();
15367
15368 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
15369 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
15370 let version = Version(0);
15371 let project_id = ProjectId(0);
15372 let file_id = FileId(0);
15373
15374 let outcome = frontend.hack_set_program(&ctx, program).await.unwrap();
15375 assert!(matches!(outcome, SetProgramOutcome::Success { .. }), "{outcome:?}");
15376 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15377 let sketch_id = sketch_object.id;
15378 let sketch = expect_sketch(sketch_object);
15379
15380 let line_id = sketch
15382 .segments
15383 .iter()
15384 .copied()
15385 .find(|seg_id| {
15386 matches!(
15387 &frontend.scene_graph.objects[seg_id.0].kind,
15388 ObjectKind::Segment {
15389 segment: Segment::Line(_)
15390 }
15391 )
15392 })
15393 .expect("Expected a line segment in sketch");
15394
15395 frontend
15397 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
15398 .await
15399 .unwrap();
15400
15401 let line_ctor = LineCtor {
15403 start: Point2d {
15404 x: Expr::Var(Number {
15405 value: 1.0,
15406 units: NumericSuffix::Mm,
15407 }),
15408 y: Expr::Var(Number {
15409 value: 2.0,
15410 units: NumericSuffix::Mm,
15411 }),
15412 },
15413 end: Point2d {
15414 x: Expr::Var(Number {
15415 value: 13.0,
15416 units: NumericSuffix::Mm,
15417 }),
15418 y: Expr::Var(Number {
15419 value: 14.0,
15420 units: NumericSuffix::Mm,
15421 }),
15422 },
15423 construction: None,
15424 };
15425 let segments = vec![ExistingSegmentCtor {
15426 id: line_id,
15427 ctor: SegmentCtor::Line(line_ctor),
15428 }];
15429 let (src_delta, _scene_delta) = frontend
15430 .edit_segments(&mock_ctx, version, sketch_id, segments)
15431 .await
15432 .unwrap();
15433 assert!(
15434 src_delta
15435 .text
15436 .contains("line(start = [var 1mm, var 2mm], end = [var 13mm, var 14mm])"),
15437 "Expected edited line in source, got: {}",
15438 src_delta.text
15439 );
15440
15441 ctx.close().await;
15442 mock_ctx.close().await;
15443 }
15444
15445 #[tokio::test(flavor = "multi_thread")]
15446 async fn test_extra_newlines_delete_segment() {
15447 let initial_source = "@settings(defaultLengthUnit = mm)
15449
15450sketch001 = sketch(on = XY) {
15451 circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
15452}
15453";
15454
15455 let program = Program::parse(initial_source).unwrap().0.unwrap();
15456 let mut frontend = FrontendState::new();
15457
15458 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
15459 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
15460 let version = Version(0);
15461
15462 frontend.hack_set_program(&ctx, program).await.unwrap();
15463 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15464 let sketch_id = sketch_object.id;
15465 let sketch = expect_sketch(sketch_object);
15466
15467 assert_eq!(sketch.segments.len(), 3);
15469 let circle_id = sketch.segments[2];
15470
15471 let (src_delta, scene_delta) = frontend
15473 .delete_objects(&mock_ctx, version, sketch_id, vec![], vec![circle_id])
15474 .await
15475 .unwrap();
15476 assert!(
15477 src_delta.text.contains("sketch(on = XY) {"),
15478 "Expected sketch block in source, got: {}",
15479 src_delta.text
15480 );
15481 let new_sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
15482 let new_sketch = expect_sketch(new_sketch_object);
15483 assert_eq!(new_sketch.segments.len(), 0);
15484
15485 ctx.close().await;
15486 mock_ctx.close().await;
15487 }
15488
15489 #[tokio::test(flavor = "multi_thread")]
15490 async fn test_unformatted_source_add_arc() {
15491 let initial_source = "@settings(defaultLengthUnit = mm)
15493
15494sketch001 = sketch(on = XY) {
15495}
15496";
15497
15498 let program = Program::parse(initial_source).unwrap().0.unwrap();
15499 let mut frontend = FrontendState::new();
15500
15501 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
15502 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
15503 let version = Version(0);
15504
15505 frontend.hack_set_program(&ctx, program).await.unwrap();
15506 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15507 let sketch_id = sketch_object.id;
15508
15509 let arc_ctor = ArcCtor {
15510 start: Point2d {
15511 x: Expr::Var(Number {
15512 value: 5.0,
15513 units: NumericSuffix::Mm,
15514 }),
15515 y: Expr::Var(Number {
15516 value: 0.0,
15517 units: NumericSuffix::Mm,
15518 }),
15519 },
15520 end: Point2d {
15521 x: Expr::Var(Number {
15522 value: 0.0,
15523 units: NumericSuffix::Mm,
15524 }),
15525 y: Expr::Var(Number {
15526 value: 5.0,
15527 units: NumericSuffix::Mm,
15528 }),
15529 },
15530 center: Point2d {
15531 x: Expr::Var(Number {
15532 value: 0.0,
15533 units: NumericSuffix::Mm,
15534 }),
15535 y: Expr::Var(Number {
15536 value: 0.0,
15537 units: NumericSuffix::Mm,
15538 }),
15539 },
15540 direction: None,
15541 construction: None,
15542 };
15543 let segment = SegmentCtor::Arc(arc_ctor);
15544 let (src_delta, scene_delta) = frontend
15545 .add_segment(&mock_ctx, version, sketch_id, segment, None)
15546 .await
15547 .unwrap();
15548 assert!(
15549 src_delta
15550 .text
15551 .contains("arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])"),
15552 "Expected arc in source, got: {}",
15553 src_delta.text
15554 );
15555 assert!(!scene_delta.new_objects.is_empty());
15556
15557 ctx.close().await;
15558 mock_ctx.close().await;
15559 }
15560
15561 #[tokio::test(flavor = "multi_thread")]
15562 async fn test_arc_direction_flows_to_source() {
15563 let initial_source = "@settings(defaultLengthUnit = mm)
15564
15565sketch001 = sketch(on = XY) {
15566}
15567";
15568
15569 let program = Program::parse(initial_source).unwrap().0.unwrap();
15570 let mut frontend = FrontendState::new();
15571
15572 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
15573 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
15574 let version = Version(0);
15575
15576 frontend.hack_set_program(&ctx, program).await.unwrap();
15577 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15578 let sketch_id = sketch_object.id;
15579
15580 let point = |x: f64, y: f64| Point2d {
15581 x: Expr::Var(Number {
15582 value: x,
15583 units: NumericSuffix::Mm,
15584 }),
15585 y: Expr::Var(Number {
15586 value: y,
15587 units: NumericSuffix::Mm,
15588 }),
15589 };
15590
15591 let arc_ctor = ArcCtor {
15593 start: point(5.0, 0.0),
15594 end: point(0.0, 5.0),
15595 center: point(0.0, 0.0),
15596 direction: Some(ArcDirection::Cw),
15597 construction: None,
15598 };
15599 let (src_delta, scene_delta) = frontend
15600 .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Arc(arc_ctor), None)
15601 .await
15602 .unwrap();
15603 assert!(
15604 src_delta.text.contains("direction = CW"),
15605 "Expected direction = CW in source, got: {}",
15606 src_delta.text
15607 );
15608 let arc_id = *scene_delta.new_objects.last().unwrap();
15610
15611 let edited_ctor = ArcCtor {
15615 start: point(0.0, -5.0),
15616 end: point(0.0, 5.0),
15617 center: point(0.0, 0.0),
15618 direction: Some(ArcDirection::Cw),
15619 construction: None,
15620 };
15621 let (src_delta, _scene_delta) = frontend
15622 .edit_segments(
15623 &mock_ctx,
15624 version,
15625 sketch_id,
15626 vec![ExistingSegmentCtor {
15627 id: arc_id,
15628 ctor: SegmentCtor::Arc(edited_ctor),
15629 }],
15630 )
15631 .await
15632 .unwrap();
15633 assert!(
15634 src_delta.text.contains("start = [var 0mm, var -5mm]"),
15635 "Expected edited start point in source, got: {}",
15636 src_delta.text
15637 );
15638 assert!(
15639 src_delta.text.contains("direction = CW"),
15640 "Expected direction = CW to be preserved in source, got: {}",
15641 src_delta.text
15642 );
15643
15644 let edited_ctor = ArcCtor {
15647 start: point(0.0, -5.0),
15648 end: point(0.0, 5.0),
15649 center: point(0.0, 0.0),
15650 direction: Some(ArcDirection::Ccw),
15651 construction: None,
15652 };
15653 let (src_delta, _scene_delta) = frontend
15654 .edit_segments(
15655 &mock_ctx,
15656 version,
15657 sketch_id,
15658 vec![ExistingSegmentCtor {
15659 id: arc_id,
15660 ctor: SegmentCtor::Arc(edited_ctor),
15661 }],
15662 )
15663 .await
15664 .unwrap();
15665 assert!(
15666 !src_delta.text.contains("direction"),
15667 "Expected direction argument to be removed from source, got: {}",
15668 src_delta.text
15669 );
15670
15671 ctx.close().await;
15672 mock_ctx.close().await;
15673 }
15674
15675 #[tokio::test(flavor = "multi_thread")]
15676 async fn test_extra_newlines_add_circle() {
15677 let initial_source = "@settings(defaultLengthUnit = mm)
15679
15680sketch001 = sketch(on = XY) {
15681}
15682";
15683
15684 let program = Program::parse(initial_source).unwrap().0.unwrap();
15685 let mut frontend = FrontendState::new();
15686
15687 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
15688 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
15689 let version = Version(0);
15690
15691 frontend.hack_set_program(&ctx, program).await.unwrap();
15692 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15693 let sketch_id = sketch_object.id;
15694
15695 let circle_ctor = CircleCtor {
15696 start: Point2d {
15697 x: Expr::Var(Number {
15698 value: 5.0,
15699 units: NumericSuffix::Mm,
15700 }),
15701 y: Expr::Var(Number {
15702 value: 0.0,
15703 units: NumericSuffix::Mm,
15704 }),
15705 },
15706 center: Point2d {
15707 x: Expr::Var(Number {
15708 value: 0.0,
15709 units: NumericSuffix::Mm,
15710 }),
15711 y: Expr::Var(Number {
15712 value: 0.0,
15713 units: NumericSuffix::Mm,
15714 }),
15715 },
15716 construction: None,
15717 };
15718 let segment = SegmentCtor::Circle(circle_ctor);
15719 let (src_delta, scene_delta) = frontend
15720 .add_segment(&mock_ctx, version, sketch_id, segment, None)
15721 .await
15722 .unwrap();
15723 assert!(
15724 src_delta
15725 .text
15726 .contains("circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])"),
15727 "Expected circle in source, got: {}",
15728 src_delta.text
15729 );
15730 assert!(!scene_delta.new_objects.is_empty());
15731
15732 ctx.close().await;
15733 mock_ctx.close().await;
15734 }
15735
15736 #[tokio::test(flavor = "multi_thread")]
15737 async fn test_extra_newlines_add_constraint() {
15738 let initial_source = "@settings(defaultLengthUnit = mm)
15740
15741sketch001 = sketch(on = XY) {
15742 line1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm])
15743 line2 = line(start = [var 10mm, var 10mm], end = [var 20mm, var 0mm])
15744}
15745";
15746
15747 let program = Program::parse(initial_source).unwrap().0.unwrap();
15748 let mut frontend = FrontendState::new();
15749
15750 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
15751 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
15752 let version = Version(0);
15753 let project_id = ProjectId(0);
15754 let file_id = FileId(0);
15755
15756 frontend.hack_set_program(&ctx, program).await.unwrap();
15757 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15758 let sketch_id = sketch_object.id;
15759 let sketch = expect_sketch(sketch_object);
15760
15761 let line_ids: Vec<ObjectId> = sketch
15763 .segments
15764 .iter()
15765 .copied()
15766 .filter(|seg_id| {
15767 matches!(
15768 &frontend.scene_graph.objects[seg_id.0].kind,
15769 ObjectKind::Segment {
15770 segment: Segment::Line(_)
15771 }
15772 )
15773 })
15774 .collect();
15775 assert_eq!(line_ids.len(), 2, "Expected two line segments");
15776
15777 let line1 = &frontend.scene_graph.objects[line_ids[0].0];
15778 let ObjectKind::Segment {
15779 segment: Segment::Line(line1_data),
15780 } = &line1.kind
15781 else {
15782 panic!("Expected line");
15783 };
15784 let line2 = &frontend.scene_graph.objects[line_ids[1].0];
15785 let ObjectKind::Segment {
15786 segment: Segment::Line(line2_data),
15787 } = &line2.kind
15788 else {
15789 panic!("Expected line");
15790 };
15791
15792 let constraint = Constraint::Coincident(Coincident {
15794 segments: vec![line1_data.end.into(), line2_data.start.into()],
15795 });
15796
15797 frontend
15799 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
15800 .await
15801 .unwrap();
15802 let (src_delta, _scene_delta) = frontend
15803 .add_constraint(&mock_ctx, version, sketch_id, constraint)
15804 .await
15805 .unwrap();
15806 assert!(
15807 src_delta.text.contains("coincident("),
15808 "Expected coincident constraint in source, got: {}",
15809 src_delta.text
15810 );
15811
15812 ctx.close().await;
15813 mock_ctx.close().await;
15814 }
15815
15816 #[tokio::test(flavor = "multi_thread")]
15817 async fn test_extra_newlines_add_line_then_edit_line() {
15818 let initial_source = "@settings(defaultLengthUnit = mm)
15820
15821sketch001 = sketch(on = XY) {
15822}
15823";
15824
15825 let program = Program::parse(initial_source).unwrap().0.unwrap();
15826 let mut frontend = FrontendState::new();
15827
15828 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
15829 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
15830 let version = Version(0);
15831
15832 frontend.hack_set_program(&ctx, program).await.unwrap();
15833 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15834 let sketch_id = sketch_object.id;
15835
15836 let line_ctor = LineCtor {
15838 start: Point2d {
15839 x: Expr::Number(Number {
15840 value: 0.0,
15841 units: NumericSuffix::Mm,
15842 }),
15843 y: Expr::Number(Number {
15844 value: 0.0,
15845 units: NumericSuffix::Mm,
15846 }),
15847 },
15848 end: Point2d {
15849 x: Expr::Number(Number {
15850 value: 10.0,
15851 units: NumericSuffix::Mm,
15852 }),
15853 y: Expr::Number(Number {
15854 value: 10.0,
15855 units: NumericSuffix::Mm,
15856 }),
15857 },
15858 construction: None,
15859 };
15860 let segment = SegmentCtor::Line(line_ctor);
15861 let (src_delta, scene_delta) = frontend
15862 .add_segment(&mock_ctx, version, sketch_id, segment, None)
15863 .await
15864 .unwrap();
15865 assert!(
15866 src_delta.text.contains("line(start = [0mm, 0mm], end = [10mm, 10mm])"),
15867 "Expected line in source after add, got: {}",
15868 src_delta.text
15869 );
15870 let line_id = *scene_delta.new_objects.last().unwrap();
15872
15873 let line_ctor = LineCtor {
15875 start: Point2d {
15876 x: Expr::Number(Number {
15877 value: 1.0,
15878 units: NumericSuffix::Mm,
15879 }),
15880 y: Expr::Number(Number {
15881 value: 2.0,
15882 units: NumericSuffix::Mm,
15883 }),
15884 },
15885 end: Point2d {
15886 x: Expr::Number(Number {
15887 value: 13.0,
15888 units: NumericSuffix::Mm,
15889 }),
15890 y: Expr::Number(Number {
15891 value: 14.0,
15892 units: NumericSuffix::Mm,
15893 }),
15894 },
15895 construction: None,
15896 };
15897 let segments = vec![ExistingSegmentCtor {
15898 id: line_id,
15899 ctor: SegmentCtor::Line(line_ctor),
15900 }];
15901 let (src_delta, scene_delta) = frontend
15902 .edit_segments(&mock_ctx, version, sketch_id, segments)
15903 .await
15904 .unwrap();
15905 assert!(
15906 src_delta.text.contains("line(start = [1mm, 2mm], end = [13mm, 14mm])"),
15907 "Expected edited line in source, got: {}",
15908 src_delta.text
15909 );
15910 assert_eq!(scene_delta.new_objects, vec![]);
15911
15912 ctx.close().await;
15913 mock_ctx.close().await;
15914 }
15915
15916 #[test]
15917 fn test_add_variable_declaration_uses_top_level_scope_after_sketch_block() {
15918 let code = "\
15927foo1 = 1
15928sk = sketch() {
15929 p = var 1.5
15930}
159317 + 8
15932";
15933 let mut ast = crate::parsing::top_level_parse(code).unwrap();
15934 let ast::BodyItem::ExpressionStatement(stmt) = &ast.body[2] else {
15935 panic!("expected an expression statement");
15936 };
15937 let source_ref = SourceRef::new(SourceRange::from(&stmt.expression), None);
15938 let (_, cmd_return) = mutate_ast_node_by_source_ref(
15939 &mut ast,
15940 &source_ref,
15941 AstMutateCommand::AddVariableDeclaration {
15942 prefix: "foo".to_owned(),
15943 },
15944 )
15945 .unwrap();
15946 let AstMutateCommandReturn::Name(name) = cmd_return else {
15947 panic!("expected a generated name");
15948 };
15949 assert_eq!(name, "foo2");
15950 let ast::BodyItem::VariableDeclaration(decl) = &ast.body[2] else {
15951 panic!("expected the expression statement to become a variable declaration");
15952 };
15953 assert_eq!(decl.name(), "foo2");
15954 }
15955
15956 fn function_body_at(ast: &ast::Node<ast::Program>, index: usize) -> &ast::Node<ast::Program> {
15958 let ast::BodyItem::VariableDeclaration(decl) = &ast.body[index] else {
15959 panic!("expected a variable declaration");
15960 };
15961 let ast::Expr::FunctionExpression(func) = &decl.declaration.init else {
15962 panic!("expected a function expression");
15963 };
15964 &func.body
15965 }
15966
15967 fn then_block_at(ast: &ast::Node<ast::Program>, index: usize) -> &ast::Node<ast::Program> {
15970 let ast::BodyItem::VariableDeclaration(decl) = &ast.body[index] else {
15971 panic!("expected a variable declaration");
15972 };
15973 let ast::Expr::IfExpression(if_expr) = &decl.declaration.init else {
15974 panic!("expected an if expression");
15975 };
15976 &if_expr.then_val
15977 }
15978
15979 #[test]
15980 fn test_add_variable_declaration_in_function_body_uses_function_scope() {
15981 let code = "\
15986fn build() {
15987 thing1 = 1
15988 10 + 20
15989 return thing1
15990}
15991";
15992 let mut ast = crate::parsing::top_level_parse(code).unwrap();
15993 let ast::BodyItem::ExpressionStatement(stmt) = &function_body_at(&ast, 0).body[1] else {
15994 panic!("expected an expression statement");
15995 };
15996 let source_ref = SourceRef::new(SourceRange::from(&stmt.expression), None);
15997 let (_, cmd_return) = mutate_ast_node_by_source_ref(
15998 &mut ast,
15999 &source_ref,
16000 AstMutateCommand::AddVariableDeclaration {
16001 prefix: "thing".to_owned(),
16002 },
16003 )
16004 .unwrap();
16005 let AstMutateCommandReturn::Name(name) = cmd_return else {
16006 panic!("expected a generated name");
16007 };
16008 assert_eq!(name, "thing2");
16009 let body = &function_body_at(&ast, 0).body;
16010 assert_eq!(body.len(), 3);
16011 let ast::BodyItem::VariableDeclaration(decl) = &body[1] else {
16012 panic!("expected the expression statement to become a variable declaration");
16013 };
16014 assert_eq!(decl.name(), "thing2");
16015 let ast::BodyItem::VariableDeclaration(first) = &body[0] else {
16017 panic!("expected a variable declaration");
16018 };
16019 assert_eq!(first.name(), "thing1");
16020 assert!(matches!(&body[2], ast::BodyItem::ReturnStatement(_)));
16021 }
16022
16023 #[test]
16024 fn test_delete_node_in_function_body_preserves_leading_comment() {
16025 let code = "\
16029fn build() {
16030 a = 1
16031 // keep me
16032 b = 2
16033 return a
16034}
16035";
16036 let mut ast = crate::parsing::top_level_parse(code).unwrap();
16037 let ast::BodyItem::VariableDeclaration(b_decl) = &function_body_at(&ast, 0).body[1] else {
16038 panic!("expected a variable declaration");
16039 };
16040 assert_eq!(b_decl.name(), "b");
16041 let source_ref = SourceRef::new(SourceRange::from(&b_decl.declaration.init), None);
16042 mutate_ast_node_by_source_ref(&mut ast, &source_ref, AstMutateCommand::DeleteNode).unwrap();
16043 let body = &function_body_at(&ast, 0).body;
16044 assert_eq!(body.len(), 2, "expected b to be deleted");
16045 let ast::BodyItem::VariableDeclaration(first) = &body[0] else {
16046 panic!("expected a variable declaration");
16047 };
16048 assert_eq!(first.name(), "a");
16049 let ast::BodyItem::ReturnStatement(_) = &body[1] else {
16050 panic!("expected the return statement to remain");
16051 };
16052 assert!(
16053 body[1].get_comments().iter().any(|c| c.contains("keep me")),
16054 "expected the deleted item's leading comment to migrate to the next item, got: {:?}",
16055 body[1].get_comments()
16056 );
16057 }
16058
16059 #[test]
16060 fn test_add_variable_declaration_in_function_body_ignores_parameters() {
16061 let code = "\
16069fn build(thing2) {
16070 thing1 = 1
16071 10 + 20
16072 return thing1 + thing2
16073}
16074";
16075 let mut ast = crate::parsing::top_level_parse(code).unwrap();
16076 let ast::BodyItem::ExpressionStatement(stmt) = &function_body_at(&ast, 0).body[1] else {
16077 panic!("expected an expression statement");
16078 };
16079 let source_ref = SourceRef::new(SourceRange::from(&stmt.expression), None);
16080 let (_, cmd_return) = mutate_ast_node_by_source_ref(
16081 &mut ast,
16082 &source_ref,
16083 AstMutateCommand::AddVariableDeclaration {
16084 prefix: "thing".to_owned(),
16085 },
16086 )
16087 .unwrap();
16088 let AstMutateCommandReturn::Name(name) = cmd_return else {
16089 panic!("expected a generated name");
16090 };
16091 assert_eq!(name, "thing2", "locals are avoided, but parameters are not");
16092 }
16093
16094 #[test]
16095 fn test_add_variable_declaration_in_if_branch_uses_branch_scope() {
16096 let code = "\
16097x = 1
16098y = if x > 0 {
16099 q1 = 1
16100 foo(q1)
16101 q1
16102} else {
16103 2
16104}
16105";
16106 let mut ast = crate::parsing::top_level_parse(code).unwrap();
16107 let ast::BodyItem::ExpressionStatement(stmt) = &then_block_at(&ast, 1).body[1] else {
16108 panic!("expected an expression statement");
16109 };
16110 let source_ref = SourceRef::new(SourceRange::from(&stmt.expression), None);
16111 let (_, cmd_return) = mutate_ast_node_by_source_ref(
16112 &mut ast,
16113 &source_ref,
16114 AstMutateCommand::AddVariableDeclaration { prefix: "q".to_owned() },
16115 )
16116 .unwrap();
16117 let AstMutateCommandReturn::Name(name) = cmd_return else {
16118 panic!("expected a generated name");
16119 };
16120 assert_eq!(name, "q2");
16121 let body = &then_block_at(&ast, 1).body;
16122 assert_eq!(body.len(), 3);
16123 let ast::BodyItem::VariableDeclaration(decl) = &body[1] else {
16124 panic!("expected the expression statement to become a variable declaration");
16125 };
16126 assert_eq!(decl.name(), "q2");
16127 let ast::BodyItem::VariableDeclaration(first) = &body[0] else {
16129 panic!("expected a variable declaration");
16130 };
16131 assert_eq!(first.name(), "q1");
16132 assert!(matches!(&body[2], ast::BodyItem::ExpressionStatement(_)));
16133 }
16134
16135 #[test]
16136 fn test_delete_node_in_if_branch_preserves_leading_comment() {
16137 let code = "\
16140y = if true {
16141 a = 1
16142 // keep me
16143 b = 2
16144 a + b
16145} else {
16146 2
16147}
16148";
16149 let mut ast = crate::parsing::top_level_parse(code).unwrap();
16150 let ast::BodyItem::VariableDeclaration(b_decl) = &then_block_at(&ast, 0).body[1] else {
16151 panic!("expected a variable declaration");
16152 };
16153 assert_eq!(b_decl.name(), "b");
16154 let source_ref = SourceRef::new(SourceRange::from(&b_decl.declaration.init), None);
16155 mutate_ast_node_by_source_ref(&mut ast, &source_ref, AstMutateCommand::DeleteNode).unwrap();
16156 let body = &then_block_at(&ast, 0).body;
16157 assert_eq!(body.len(), 2, "expected b to be deleted");
16158 let ast::BodyItem::VariableDeclaration(first) = &body[0] else {
16159 panic!("expected a variable declaration");
16160 };
16161 assert_eq!(first.name(), "a");
16162 let ast::BodyItem::ExpressionStatement(_) = &body[1] else {
16163 panic!("expected the tail expression to remain");
16164 };
16165 assert!(
16166 body[1].get_comments().iter().any(|c| c.contains("keep me")),
16167 "expected the deleted item's leading comment to migrate to the next item, got: {:?}",
16168 body[1].get_comments()
16169 );
16170 }
16171}