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: checkpoint.exec_outcome,
585 },
586 })
587 }
588
589 pub fn clear_sketch_checkpoints(&mut self) {
590 self.sketch_checkpoints.clear();
591 }
592 fn scene_graph_for_ui(&self) -> SceneGraph {
593 let has_control_point_splines = self.scene_graph.objects.iter().any(|object| {
594 matches!(
595 object.kind,
596 ObjectKind::Segment {
597 segment: Segment::ControlPointSpline(_)
598 }
599 )
600 });
601
602 if !has_control_point_splines {
603 return self.scene_graph.clone();
604 }
605
606 let hidden_constraint_ids = self
607 .scene_graph
608 .objects
609 .iter()
610 .filter_map(|object| match &object.kind {
611 ObjectKind::Constraint {
612 constraint: Constraint::Coincident(coincident),
613 } if coincident_is_internal_to_same_control_point_spline(coincident, &self.scene_graph) => {
614 Some(object.id)
615 }
616 _ => None,
617 })
618 .collect::<HashSet<_>>();
619
620 if hidden_constraint_ids.is_empty() {
621 return self.scene_graph.clone();
622 }
623
624 let mut scene_graph = self.scene_graph.clone();
625 for object in &mut scene_graph.objects {
626 match &mut object.kind {
627 ObjectKind::Constraint { .. } if hidden_constraint_ids.contains(&object.id) => {
628 object.kind = ObjectKind::Nil;
629 }
630 ObjectKind::Sketch(sketch) => {
631 sketch
632 .constraints
633 .retain(|constraint_id| !hidden_constraint_ids.contains(constraint_id));
634 }
635 _ => {}
636 }
637 }
638
639 scene_graph
640 }
641}
642
643fn coincident_is_internal_to_same_control_point_spline(coincident: &Coincident, scene_graph: &SceneGraph) -> bool {
644 let mut first_owner_id = None;
645 for segment_id in coincident.segment_ids() {
646 let Some(owner_id) = owning_control_point_spline_id(segment_id, scene_graph) else {
647 return false;
648 };
649
650 match first_owner_id {
651 Some(first_owner_id) if first_owner_id != owner_id => return false,
652 Some(_) => {}
653 None => first_owner_id = Some(owner_id),
654 }
655 }
656
657 first_owner_id.is_some()
658}
659
660fn owning_control_point_spline_id(segment_id: ObjectId, scene_graph: &SceneGraph) -> Option<ObjectId> {
661 let object = scene_graph.objects.get(segment_id.0)?;
662 let ObjectKind::Segment { segment } = &object.kind else {
663 return None;
664 };
665
666 match segment {
667 Segment::ControlPointSpline(_) => Some(segment_id),
668 Segment::Point(point) => point
669 .owner
670 .filter(|owner_id| matches_control_point_spline_owner(*owner_id, scene_graph)),
671 Segment::Line(line) => line
672 .owner
673 .filter(|owner_id| matches_control_point_spline_owner(*owner_id, scene_graph)),
674 _ => None,
675 }
676}
677
678fn matches_control_point_spline_owner(owner_id: ObjectId, scene_graph: &SceneGraph) -> bool {
679 matches!(
680 scene_graph.objects.get(owner_id.0).map(|object| &object.kind),
681 Some(ObjectKind::Segment {
682 segment: Segment::ControlPointSpline(_)
683 })
684 )
685}
686
687fn ensure_control_point_spline_experimental_features(program: &Program) -> Result<Program, KclError> {
688 let experimental_features_allowed = program
689 .meta_settings()
690 .ok()
691 .flatten()
692 .map(|settings| settings.experimental_features == WarningLevel::Allow)
693 .unwrap_or(false);
694 if experimental_features_allowed {
695 return Ok(program.clone());
696 }
697
698 program.change_experimental_features(Some(WarningLevel::Allow))
699}
700
701impl SketchApi for FrontendState {
702 async fn execute_mock(
703 &mut self,
704 ctx: &ExecutorContext,
705 _version: Version,
706 sketch: ObjectId,
707 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
708 let sketch_block_ref =
709 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
710
711 let mut truncated_program = self.program.clone();
712 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::None)
713 .map_err(KclErrorWithOutputs::no_outputs)?;
714
715 let outcome = ctx
717 .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch))
718 .await?;
719 let new_source = source_from_ast(&self.program.ast);
720 let src_delta = SourceDelta { text: new_source };
721 let outcome = self.update_state_after_exec(outcome, true);
723 let scene_graph_delta = SceneGraphDelta {
724 new_graph: self.scene_graph.clone(),
725 new_objects: Default::default(),
726 invalidates_ids: false,
727 exec_outcome: outcome,
728 };
729 Ok((src_delta, scene_graph_delta))
730 }
731
732 async fn new_sketch(
733 &mut self,
734 ctx: &ExecutorContext,
735 _project: ProjectId,
736 _file: FileId,
737 _version: Version,
738 args: SketchCtor,
739 ) -> ExecResult<(SourceDelta, SceneGraphDelta, ObjectId)> {
740 let mut new_ast = self.program.ast.clone();
743 let mut plane_ast = sketch_on_ast_expr(&mut new_ast, &self.scene_graph, &self.solid_references, &args.on)
745 .map_err(KclErrorWithOutputs::no_outputs)?;
746 let mut defined_names = find_defined_names(&new_ast);
747 let is_face_of_expr = matches!(
748 &plane_ast,
749 ast::Expr::CallExpressionKw(call) if call.callee.name.name == "faceOf"
750 );
751 if is_face_of_expr {
752 let face_name = next_free_name_with_padding("face", &defined_names)
753 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
754 let face_decl = ast::VariableDeclaration::new(
755 ast::VariableDeclarator::new(&face_name, plane_ast),
756 ast::ItemVisibility::Default,
757 ast::VariableKind::Const,
758 );
759 new_ast
760 .body
761 .push(ast::BodyItem::VariableDeclaration(BoxNode::new(ast::Node::no_src(
762 face_decl,
763 ))));
764 defined_names.insert(face_name.clone());
765 plane_ast = ast::Expr::Name(BoxNode::new(ast::Name::new(&face_name)));
766 }
767 let sketch_ast = ast::SketchBlock {
768 arguments: vec![ast::LabeledArg {
769 label: Some(ast::Identifier::new(SKETCH_BLOCK_PARAM_ON)),
770 arg: plane_ast,
771 }],
772 body: Default::default(),
773 is_being_edited: false,
774 non_code_meta: Default::default(),
775 digest: None,
776 };
777 let sketch_name = next_free_name_with_padding("sketch", &defined_names)
780 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
781 let sketch_decl = ast::VariableDeclaration::new(
782 ast::VariableDeclarator::new(
783 &sketch_name,
784 ast::Expr::SketchBlock(BoxNode::new(ast::Node::no_src(sketch_ast))),
785 ),
786 ast::ItemVisibility::Default,
787 ast::VariableKind::Const,
788 );
789 new_ast
790 .body
791 .push(ast::BodyItem::VariableDeclaration(BoxNode::new(ast::Node::no_src(
792 sketch_decl,
793 ))));
794 let new_source = source_from_ast(&new_ast);
796 let new_program = parse_frontend_mutation_source(
798 &new_source,
799 "Error parsing KCL source after adding sketch",
800 "No AST produced after adding sketch",
801 )?;
802
803 self.program = new_program.clone();
805
806 let outcome = ctx.run_with_caching(new_program.clone()).await?;
809 let freedom_analysis_ran = true;
810
811 let outcome = self.update_state_after_exec(outcome, freedom_analysis_ran);
812
813 let Some(sketch_id) = self
814 .scene_graph
815 .objects
816 .iter()
817 .filter_map(|object| match object.kind {
818 ObjectKind::Sketch(_) => Some(object.id),
819 _ => None,
820 })
821 .max_by_key(|id| id.0)
822 else {
823 return Err(KclErrorWithOutputs::from_error_outcome(
824 KclError::refactor("No objects in scene graph after adding sketch".to_owned()),
825 outcome,
826 ));
827 };
828 self.scene_graph.sketch_mode = Some(sketch_id);
830
831 let src_delta = SourceDelta { text: new_source };
832 let scene_graph_delta = SceneGraphDelta {
833 new_graph: self.scene_graph_for_ui(),
834 invalidates_ids: false,
835 new_objects: vec![sketch_id],
836 exec_outcome: outcome,
837 };
838 Ok((src_delta, scene_graph_delta, sketch_id))
839 }
840
841 async fn edit_sketch(
842 &mut self,
843 ctx: &ExecutorContext,
844 _project: ProjectId,
845 _file: FileId,
846 _version: Version,
847 sketch: ObjectId,
848 ) -> ExecResult<SceneGraphDelta> {
849 let sketch_object = self.scene_graph.objects.get(sketch.0).ok_or_else(|| {
853 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
854 })?;
855 let ObjectKind::Sketch(_) = &sketch_object.kind else {
856 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
857 "Object is not a sketch, it is {}",
858 sketch_object.kind.human_friendly_kind_with_article()
859 ))));
860 };
861 let sketch_block_ref = expect_single_node_ref(sketch_object).map_err(KclErrorWithOutputs::no_outputs)?;
862
863 self.scene_graph.sketch_mode = Some(sketch);
865
866 let mut truncated_program = self.program.clone();
868 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::None)
869 .map_err(KclErrorWithOutputs::no_outputs)?;
870
871 let outcome = ctx
874 .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch))
875 .await?;
876
877 let outcome = self.update_state_after_exec(outcome, true);
879 let scene_graph_delta = SceneGraphDelta {
880 new_graph: self.scene_graph_for_ui(),
881 invalidates_ids: false,
882 new_objects: Vec::new(),
883 exec_outcome: outcome,
884 };
885 Ok(scene_graph_delta)
886 }
887
888 async fn exit_sketch(
889 &mut self,
890 ctx: &ExecutorContext,
891 _version: Version,
892 sketch: ObjectId,
893 ) -> ExecResult<SceneGraph> {
894 #[cfg(not(target_arch = "wasm32"))]
896 let _ = sketch;
897 #[cfg(target_arch = "wasm32")]
898 if self.scene_graph.sketch_mode != Some(sketch) {
899 web_sys::console::warn_1(
900 &format!(
901 "WARNING: exit_sketch: current state's sketch mode ID doesn't match the given sketch ID; state={:#?}, given={sketch:?}",
902 self.scene_graph.sketch_mode
903 )
904 .into(),
905 );
906 }
907 self.scene_graph.sketch_mode = None;
908
909 let outcome = ctx.run_with_caching(self.program.clone()).await?;
911
912 self.update_state_after_exec(outcome, false);
914
915 Ok(self.scene_graph_for_ui())
916 }
917
918 async fn delete_sketch(
919 &mut self,
920 ctx: &ExecutorContext,
921 _version: Version,
922 sketch: ObjectId,
923 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
924 let mut new_ast = self.program.ast.clone();
927
928 let sketch_id = sketch;
930 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
931 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
932 })?;
933 let ObjectKind::Sketch(_) = &sketch_object.kind else {
934 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
935 "Object is not a sketch, it is {}",
936 sketch_object.kind.human_friendly_kind_with_article(),
937 ))));
938 };
939
940 self.mutate_ast(&mut new_ast, sketch_id, AstMutateCommand::DeleteNode)
942 .map_err(KclErrorWithOutputs::no_outputs)?;
943
944 self.execute_after_delete_sketch(ctx, &mut new_ast).await
945 }
946
947 async fn add_segment(
948 &mut self,
949 ctx: &ExecutorContext,
950 _version: Version,
951 sketch: ObjectId,
952 segment: SegmentCtor,
953 _label: Option<String>,
954 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
955 match segment {
957 SegmentCtor::Point(ctor) => self.add_point(ctx, sketch, ctor).await,
958 SegmentCtor::Line(ctor) => self.add_line(ctx, sketch, ctor).await,
959 SegmentCtor::Arc(ctor) => self.add_arc(ctx, sketch, ctor).await,
960 SegmentCtor::Circle(ctor) => self.add_circle(ctx, sketch, ctor).await,
961 SegmentCtor::ControlPointSpline(ctor) => self.add_control_point_spline(ctx, sketch, ctor).await,
962 }
963 }
964
965 async fn edit_segments(
966 &mut self,
967 ctx: &ExecutorContext,
968 _version: Version,
969 sketch: ObjectId,
970 segments: Vec<ExistingSegmentCtor>,
971 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
972 let sketch_block_ref =
974 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
975
976 let mut new_ast = self.program.ast.clone();
977 let mut edited_segment_ids = AhashIndexSet::with_capacity_and_hasher(segments.len(), Default::default());
978 let mut invalidates_ids = false;
979
980 for segment in &segments {
983 edited_segment_ids.insert(segment.id);
984 if let SegmentCtor::ControlPointSpline(new_ctor) = &segment.ctor
985 && let Some(existing_object) = self.scene_graph.objects.get(segment.id.0)
986 && let ObjectKind::Segment {
987 segment: Segment::ControlPointSpline(existing_spline),
988 } = &existing_object.kind
989 && existing_spline.controls.len() != new_ctor.points.len()
990 {
991 invalidates_ids = true;
992 }
993 }
994 let drag_anchor_segment_ids = self
995 .next_drag_anchor_segment_ids
996 .take()
997 .unwrap_or_else(|| edited_segment_ids.clone());
998 let constraint_label_edits = self.next_constraint_label_edits.take().unwrap_or_default();
999 let commit_solved_initial_guesses = self.next_edit_commits_solver_solutions.take().unwrap_or(true);
1000
1001 let mut final_edits: IndexMap<ObjectId, SegmentCtor> = IndexMap::new();
1016
1017 for segment in segments {
1018 let segment_id = segment.id;
1019 match segment.ctor {
1020 SegmentCtor::Point(ctor) => {
1021 if let Some(segment_object) = self.scene_graph.objects.get(segment_id.0)
1023 && let ObjectKind::Segment { segment } = &segment_object.kind
1024 && let Segment::Point(point) = segment
1025 && let Some(owner_id) = point.owner
1026 && let Some(owner_object) = self.scene_graph.objects.get(owner_id.0)
1027 && let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind
1028 {
1029 match owner_segment {
1030 Segment::Line(line) if line.start == segment_id || line.end == segment_id => {
1031 if let Some(existing) = final_edits.get_mut(&owner_id) {
1032 let SegmentCtor::Line(line_ctor) = existing else {
1033 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1034 "Internal: Expected line ctor for owner, but found {}",
1035 existing.human_friendly_kind_with_article()
1036 ))));
1037 };
1038 if line.start == segment_id {
1040 line_ctor.start = ctor.position;
1041 } else {
1042 line_ctor.end = ctor.position;
1043 }
1044 } else if let SegmentCtor::Line(line_ctor) = &line.ctor {
1045 let mut line_ctor = line_ctor.clone();
1047 if line.start == segment_id {
1048 line_ctor.start = ctor.position;
1049 } else {
1050 line_ctor.end = ctor.position;
1051 }
1052 final_edits.insert(owner_id, SegmentCtor::Line(line_ctor));
1053 } else {
1054 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1056 "Internal: Line does not have line ctor, but found {}",
1057 line.ctor.human_friendly_kind_with_article()
1058 ))));
1059 }
1060 continue;
1061 }
1062 Segment::Arc(arc)
1063 if arc.start == segment_id || arc.end == segment_id || arc.center == segment_id =>
1064 {
1065 if let Some(existing) = final_edits.get_mut(&owner_id) {
1066 let SegmentCtor::Arc(arc_ctor) = existing else {
1067 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1068 "Internal: Expected arc ctor for owner, but found {}",
1069 existing.human_friendly_kind_with_article()
1070 ))));
1071 };
1072 if arc.start == segment_id {
1073 arc_ctor.start = ctor.position;
1074 } else if arc.end == segment_id {
1075 arc_ctor.end = ctor.position;
1076 } else {
1077 arc_ctor.center = ctor.position;
1078 }
1079 } else if let SegmentCtor::Arc(arc_ctor) = &arc.ctor {
1080 let mut arc_ctor = arc_ctor.clone();
1081 if arc.start == segment_id {
1082 arc_ctor.start = ctor.position;
1083 } else if arc.end == segment_id {
1084 arc_ctor.end = ctor.position;
1085 } else {
1086 arc_ctor.center = ctor.position;
1087 }
1088 final_edits.insert(owner_id, SegmentCtor::Arc(arc_ctor));
1089 } else {
1090 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1091 "Internal: Arc does not have arc ctor, but found {}",
1092 arc.ctor.human_friendly_kind_with_article()
1093 ))));
1094 }
1095 continue;
1096 }
1097 Segment::Circle(circle) if circle.start == segment_id || circle.center == segment_id => {
1098 if let Some(existing) = final_edits.get_mut(&owner_id) {
1099 let SegmentCtor::Circle(circle_ctor) = existing else {
1100 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1101 "Internal: Expected circle ctor for owner, but found {}",
1102 existing.human_friendly_kind_with_article()
1103 ))));
1104 };
1105 if circle.start == segment_id {
1106 circle_ctor.start = ctor.position;
1107 } else {
1108 circle_ctor.center = ctor.position;
1109 }
1110 } else if let SegmentCtor::Circle(circle_ctor) = &circle.ctor {
1111 let mut circle_ctor = circle_ctor.clone();
1112 if circle.start == segment_id {
1113 circle_ctor.start = ctor.position;
1114 } else {
1115 circle_ctor.center = ctor.position;
1116 }
1117 final_edits.insert(owner_id, SegmentCtor::Circle(circle_ctor));
1118 } else {
1119 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1120 "Internal: Circle does not have circle ctor, but found {}",
1121 circle.ctor.human_friendly_kind_with_article()
1122 ))));
1123 }
1124 continue;
1125 }
1126 Segment::ControlPointSpline(spline) if spline.controls.contains(&segment_id) => {
1127 let Some(control_index) =
1128 spline.controls.iter().position(|control_id| *control_id == segment_id)
1129 else {
1130 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1131 "Internal: Point is not part of owner's controlPointSpline segment: point={segment_id:?}, spline={owner_id:?}"
1132 ))));
1133 };
1134 if let Some(existing) = final_edits.get_mut(&owner_id) {
1135 let SegmentCtor::ControlPointSpline(spline_ctor) = existing else {
1136 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1137 "Internal: Expected controlPointSpline ctor for owner, but found {}",
1138 existing.human_friendly_kind_with_article()
1139 ))));
1140 };
1141 spline_ctor.points[control_index] = ctor.position;
1142 } else if let SegmentCtor::ControlPointSpline(spline_ctor) = &spline.ctor {
1143 let mut spline_ctor = spline_ctor.clone();
1144 spline_ctor.points[control_index] = ctor.position;
1145 final_edits.insert(owner_id, SegmentCtor::ControlPointSpline(spline_ctor));
1146 } else {
1147 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1148 "Internal: Control point spline does not have controlPointSpline ctor, but found {}",
1149 spline.ctor.human_friendly_kind_with_article()
1150 ))));
1151 }
1152 continue;
1153 }
1154 _ => {}
1155 }
1156 }
1157
1158 final_edits.insert(segment_id, SegmentCtor::Point(ctor));
1160 }
1161 SegmentCtor::Line(ctor) => {
1162 final_edits.insert(segment_id, SegmentCtor::Line(ctor));
1163 }
1164 SegmentCtor::Arc(ctor) => {
1165 final_edits.insert(segment_id, SegmentCtor::Arc(ctor));
1166 }
1167 SegmentCtor::Circle(ctor) => {
1168 final_edits.insert(segment_id, SegmentCtor::Circle(ctor));
1169 }
1170 SegmentCtor::ControlPointSpline(ctor) => {
1171 final_edits.insert(segment_id, SegmentCtor::ControlPointSpline(ctor));
1172 }
1173 }
1174 }
1175
1176 for (segment_id, ctor) in final_edits {
1177 match ctor {
1178 SegmentCtor::Point(ctor) => self
1179 .edit_point(&mut new_ast, sketch, segment_id, ctor)
1180 .map_err(KclErrorWithOutputs::no_outputs)?,
1181 SegmentCtor::Line(ctor) => self
1182 .edit_line(&mut new_ast, sketch, segment_id, ctor)
1183 .map_err(KclErrorWithOutputs::no_outputs)?,
1184 SegmentCtor::Arc(ctor) => self
1185 .edit_arc(&mut new_ast, sketch, segment_id, ctor)
1186 .map_err(KclErrorWithOutputs::no_outputs)?,
1187 SegmentCtor::Circle(ctor) => self
1188 .edit_circle(&mut new_ast, sketch, segment_id, ctor)
1189 .map_err(KclErrorWithOutputs::no_outputs)?,
1190 SegmentCtor::ControlPointSpline(ctor) => self
1191 .edit_control_point_spline(&mut new_ast, sketch, segment_id, ctor)
1192 .map_err(KclErrorWithOutputs::no_outputs)?,
1193 }
1194 }
1195 for edit in constraint_label_edits {
1196 self.mutate_constraint_label_position(&mut new_ast, edit.constraint_id, edit.label_position)
1197 .map_err(KclErrorWithOutputs::no_outputs)?;
1198 }
1199 let (source_delta, mut scene_graph_delta) = self
1200 .execute_after_edit(
1201 ctx,
1202 sketch,
1203 sketch_block_ref,
1204 &mut new_ast,
1205 ExecuteAfterEditOptions {
1206 segment_ids_edited: drag_anchor_segment_ids,
1207 edit_kind: EditDeleteKind::Edit,
1208 commit_solved_initial_guesses,
1209 },
1210 )
1211 .await?;
1212 if invalidates_ids {
1213 scene_graph_delta.invalidates_ids = true;
1214 }
1215 Ok((source_delta, scene_graph_delta))
1216 }
1217
1218 async fn delete_objects(
1219 &mut self,
1220 ctx: &ExecutorContext,
1221 _version: Version,
1222 sketch: ObjectId,
1223 constraint_ids: Vec<ObjectId>,
1224 segment_ids: Vec<ObjectId>,
1225 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1226 let sketch_block_ref =
1228 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1229
1230 let mut constraint_ids_set = constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
1232 let segment_ids_set = segment_ids.into_iter().collect::<AhashIndexSet<_>>();
1233
1234 let mut resolved_segment_ids_to_delete = AhashIndexSet::default();
1237
1238 for segment_id in segment_ids_set.iter().copied() {
1239 let owner_id = self.scene_graph.objects.get(segment_id.0).and_then(|segment_object| {
1240 let ObjectKind::Segment { segment } = &segment_object.kind else {
1241 return None;
1242 };
1243 match segment {
1244 Segment::Point(point) => point.owner,
1245 Segment::Line(line) => line.owner,
1246 _ => None,
1247 }
1248 });
1249
1250 if let Some(owner_id) = owner_id
1251 && let Some(owner_object) = self.scene_graph.objects.get(owner_id.0)
1252 && let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind
1253 && matches!(
1254 owner_segment,
1255 Segment::Line(_) | Segment::Arc(_) | Segment::Circle(_) | Segment::ControlPointSpline(_)
1256 )
1257 {
1258 resolved_segment_ids_to_delete.insert(owner_id);
1260 } else {
1261 resolved_segment_ids_to_delete.insert(segment_id);
1263 }
1264 }
1265 let referenced_constraint_ids = self
1266 .find_referenced_constraints(sketch, &resolved_segment_ids_to_delete)
1267 .map_err(KclErrorWithOutputs::no_outputs)?;
1268
1269 let mut new_ast = self.program.ast.clone();
1270
1271 for constraint_id in referenced_constraint_ids {
1272 if constraint_ids_set.contains(&constraint_id) {
1273 continue;
1274 }
1275
1276 let constraint_object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
1277 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Constraint not found: {constraint_id:?}")))
1278 })?;
1279 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
1280 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1281 "Object is not a constraint, it is {}",
1282 constraint_object.kind.human_friendly_kind_with_article()
1283 ))));
1284 };
1285
1286 match constraint {
1287 Constraint::Coincident(coincident) => {
1288 let remaining_segments =
1289 self.remaining_constraint_segments(&coincident.segments, &resolved_segment_ids_to_delete);
1290
1291 if remaining_segments.len() >= 2 {
1293 self.edit_coincident_constraint(&mut new_ast, constraint_id, remaining_segments)
1294 .map_err(KclErrorWithOutputs::no_outputs)?;
1295 } else {
1296 constraint_ids_set.insert(constraint_id);
1297 }
1298 }
1299 Constraint::EqualRadius(equal_radius) => {
1300 let remaining_input = equal_radius
1301 .input
1302 .iter()
1303 .copied()
1304 .filter(|segment_id| {
1305 !self.segment_will_be_deleted(*segment_id, &resolved_segment_ids_to_delete)
1306 })
1307 .collect::<Vec<_>>();
1308
1309 if remaining_input.len() >= 2 {
1310 self.edit_equal_radius_constraint(&mut new_ast, constraint_id, remaining_input)
1311 .map_err(KclErrorWithOutputs::no_outputs)?;
1312 } else {
1313 constraint_ids_set.insert(constraint_id);
1314 }
1315 }
1316 Constraint::LinesEqualLength(lines_equal_length) => {
1317 let remaining_lines = lines_equal_length
1318 .lines
1319 .iter()
1320 .copied()
1321 .filter(|line_id| !self.segment_will_be_deleted(*line_id, &resolved_segment_ids_to_delete))
1322 .collect::<Vec<_>>();
1323
1324 if remaining_lines.len() >= 2 {
1326 self.edit_equal_length_constraint(&mut new_ast, constraint_id, remaining_lines)
1327 .map_err(KclErrorWithOutputs::no_outputs)?;
1328 } else {
1329 constraint_ids_set.insert(constraint_id);
1330 }
1331 }
1332 Constraint::Parallel(parallel) => {
1333 let remaining_lines = parallel
1334 .lines
1335 .iter()
1336 .copied()
1337 .filter(|line_id| !self.segment_will_be_deleted(*line_id, &resolved_segment_ids_to_delete))
1338 .collect::<Vec<_>>();
1339
1340 if remaining_lines.len() >= 2 {
1341 self.edit_parallel_constraint(&mut new_ast, constraint_id, remaining_lines)
1342 .map_err(KclErrorWithOutputs::no_outputs)?;
1343 } else {
1344 constraint_ids_set.insert(constraint_id);
1345 }
1346 }
1347 Constraint::Horizontal(Horizontal::Points { points }) => {
1348 let remaining_points = self.remaining_constraint_segments(points, &resolved_segment_ids_to_delete);
1349
1350 if remaining_points.len() >= 2 {
1351 self.edit_horizontal_points_constraint(&mut new_ast, constraint_id, remaining_points)
1352 .map_err(KclErrorWithOutputs::no_outputs)?;
1353 } else {
1354 constraint_ids_set.insert(constraint_id);
1355 }
1356 }
1357 Constraint::Vertical(Vertical::Points { points }) => {
1358 let remaining_points = self.remaining_constraint_segments(points, &resolved_segment_ids_to_delete);
1359
1360 if remaining_points.len() >= 2 {
1361 self.edit_vertical_points_constraint(&mut new_ast, constraint_id, remaining_points)
1362 .map_err(KclErrorWithOutputs::no_outputs)?;
1363 } else {
1364 constraint_ids_set.insert(constraint_id);
1365 }
1366 }
1367 Constraint::Fixed(fixed) => {
1368 if fixed.points.iter().any(|fixed_point| {
1369 self.segment_will_be_deleted(fixed_point.point, &resolved_segment_ids_to_delete)
1370 }) {
1371 constraint_ids_set.insert(constraint_id);
1372 }
1373 }
1374 _ => {
1375 constraint_ids_set.insert(constraint_id);
1377 }
1378 }
1379 }
1380
1381 for constraint_id in constraint_ids_set {
1382 self.delete_constraint(&mut new_ast, sketch, constraint_id)
1383 .map_err(KclErrorWithOutputs::no_outputs)?;
1384 }
1385 for segment_id in resolved_segment_ids_to_delete {
1386 self.delete_segment(&mut new_ast, sketch, segment_id)
1387 .map_err(KclErrorWithOutputs::no_outputs)?;
1388 }
1389
1390 self.execute_after_edit(
1391 ctx,
1392 sketch,
1393 sketch_block_ref,
1394 &mut new_ast,
1395 ExecuteAfterEditOptions {
1396 segment_ids_edited: Default::default(),
1397 edit_kind: EditDeleteKind::DeleteNonSketch,
1398 commit_solved_initial_guesses: true,
1399 },
1400 )
1401 .await
1402 }
1403
1404 async fn add_constraint(
1405 &mut self,
1406 ctx: &ExecutorContext,
1407 _version: Version,
1408 sketch: ObjectId,
1409 constraint: Constraint,
1410 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1411 let original_program = self.program.clone();
1415 let original_scene_graph = self.scene_graph.clone();
1416
1417 let mut new_ast = self.program.ast.clone();
1418 let sketch_block_ref = match constraint {
1419 Constraint::Coincident(coincident) => self
1420 .add_coincident(sketch, coincident, &mut new_ast)
1421 .await
1422 .map_err(KclErrorWithOutputs::no_outputs)?,
1423 Constraint::Distance(distance) => self
1424 .add_distance(sketch, distance, &mut new_ast)
1425 .await
1426 .map_err(KclErrorWithOutputs::no_outputs)?,
1427 Constraint::EqualRadius(equal_radius) => self
1428 .add_equal_radius(sketch, equal_radius, &mut new_ast)
1429 .await
1430 .map_err(KclErrorWithOutputs::no_outputs)?,
1431 Constraint::Fixed(fixed) => self
1432 .add_fixed_constraints(sketch, fixed.points, &mut new_ast)
1433 .await
1434 .map_err(KclErrorWithOutputs::no_outputs)?,
1435 Constraint::HorizontalDistance(distance) => self
1436 .add_horizontal_distance(sketch, distance, &mut new_ast)
1437 .await
1438 .map_err(KclErrorWithOutputs::no_outputs)?,
1439 Constraint::VerticalDistance(distance) => self
1440 .add_vertical_distance(sketch, distance, &mut new_ast)
1441 .await
1442 .map_err(KclErrorWithOutputs::no_outputs)?,
1443 Constraint::Horizontal(horizontal) => self
1444 .add_horizontal(sketch, horizontal, &mut new_ast)
1445 .await
1446 .map_err(KclErrorWithOutputs::no_outputs)?,
1447 Constraint::LinesEqualLength(lines_equal_length) => self
1448 .add_lines_equal_length(sketch, lines_equal_length, &mut new_ast)
1449 .await
1450 .map_err(KclErrorWithOutputs::no_outputs)?,
1451 Constraint::Midpoint(midpoint) => self
1452 .add_midpoint(sketch, midpoint, &mut new_ast)
1453 .await
1454 .map_err(KclErrorWithOutputs::no_outputs)?,
1455 Constraint::Parallel(parallel) => self
1456 .add_parallel(sketch, parallel, &mut new_ast)
1457 .await
1458 .map_err(KclErrorWithOutputs::no_outputs)?,
1459 Constraint::Perpendicular(perpendicular) => self
1460 .add_perpendicular(sketch, perpendicular, &mut new_ast)
1461 .await
1462 .map_err(KclErrorWithOutputs::no_outputs)?,
1463 Constraint::Radius(radius) => self
1464 .add_radius(sketch, radius, &mut new_ast)
1465 .await
1466 .map_err(KclErrorWithOutputs::no_outputs)?,
1467 Constraint::Diameter(diameter) => self
1468 .add_diameter(sketch, diameter, &mut new_ast)
1469 .await
1470 .map_err(KclErrorWithOutputs::no_outputs)?,
1471 Constraint::Symmetric(symmetric) => self
1472 .add_symmetric(sketch, symmetric, &mut new_ast)
1473 .await
1474 .map_err(KclErrorWithOutputs::no_outputs)?,
1475 Constraint::Vertical(vertical) => self
1476 .add_vertical(sketch, vertical, &mut new_ast)
1477 .await
1478 .map_err(KclErrorWithOutputs::no_outputs)?,
1479 Constraint::Angle(lines_at_angle) => self
1480 .add_angle(sketch, lines_at_angle, &mut new_ast)
1481 .await
1482 .map_err(KclErrorWithOutputs::no_outputs)?,
1483 Constraint::Tangent(tangent) => self
1484 .add_tangent(sketch, tangent, &mut new_ast)
1485 .await
1486 .map_err(KclErrorWithOutputs::no_outputs)?,
1487 };
1488
1489 let result = self
1490 .execute_after_add_constraint(ctx, sketch, sketch_block_ref, &mut new_ast)
1491 .await;
1492
1493 if result.is_err() {
1495 self.program = original_program;
1496 self.scene_graph = original_scene_graph;
1497 }
1498
1499 result
1500 }
1501
1502 async fn chain_segment(
1503 &mut self,
1504 ctx: &ExecutorContext,
1505 version: Version,
1506 sketch: ObjectId,
1507 previous_segment_end_point_id: ObjectId,
1508 segment: SegmentCtor,
1509 _label: Option<String>,
1510 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1511 let SegmentCtor::Line(line_ctor) = segment else {
1515 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1516 "chain_segment currently only supports Line segments, got {}",
1517 segment.human_friendly_kind_with_article(),
1518 ))));
1519 };
1520
1521 let (_first_src_delta, first_scene_delta) = self.add_line(ctx, sketch, line_ctor).await?;
1523
1524 let new_line_id = first_scene_delta
1527 .new_objects
1528 .iter()
1529 .find(|&obj_id| {
1530 let obj = self.scene_graph.objects.get(obj_id.0);
1531 if let Some(obj) = obj {
1532 matches!(
1533 &obj.kind,
1534 ObjectKind::Segment {
1535 segment: Segment::Line(_)
1536 }
1537 )
1538 } else {
1539 false
1540 }
1541 })
1542 .ok_or_else(|| {
1543 KclErrorWithOutputs::no_outputs(KclError::refactor(
1544 "Failed to find new line segment in scene graph".to_string(),
1545 ))
1546 })?;
1547
1548 let new_line_obj = self.scene_graph.objects.get(new_line_id.0).ok_or_else(|| {
1549 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1550 "New line object not found: {new_line_id:?}"
1551 )))
1552 })?;
1553
1554 let ObjectKind::Segment {
1555 segment: new_line_segment,
1556 } = &new_line_obj.kind
1557 else {
1558 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1559 "Object is not a segment: {new_line_obj:?}"
1560 ))));
1561 };
1562
1563 let Segment::Line(new_line) = new_line_segment else {
1564 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1565 "Segment is not a line: {new_line_segment:?}"
1566 ))));
1567 };
1568
1569 let new_line_start_point_id = new_line.start;
1570
1571 let coincident = Coincident {
1573 segments: vec![previous_segment_end_point_id.into(), new_line_start_point_id.into()],
1574 };
1575
1576 let (final_src_delta, final_scene_delta) = self
1577 .add_constraint(ctx, version, sketch, Constraint::Coincident(coincident))
1578 .await?;
1579
1580 let mut combined_new_objects = first_scene_delta.new_objects.clone();
1583 combined_new_objects.extend(final_scene_delta.new_objects);
1584
1585 let scene_graph_delta = SceneGraphDelta {
1586 new_graph: self.scene_graph_for_ui(),
1587 invalidates_ids: false,
1588 new_objects: combined_new_objects,
1589 exec_outcome: final_scene_delta.exec_outcome,
1590 };
1591
1592 Ok((final_src_delta, scene_graph_delta))
1593 }
1594
1595 async fn edit_constraint_value(
1597 &mut self,
1598 ctx: &ExecutorContext,
1599 _version: Version,
1600 sketch: ObjectId,
1601 constraint_id: ObjectId,
1602 value_expression: String,
1603 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1604 let sketch_block_ref =
1606 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1607
1608 let object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
1609 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Object not found: {constraint_id:?}")))
1610 })?;
1611 if !matches!(&object.kind, ObjectKind::Constraint { .. }) {
1612 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1613 "Object is not a constraint: {constraint_id:?}"
1614 ))));
1615 }
1616
1617 let mut new_ast = self.program.ast.clone();
1618
1619 let (parsed, errors) = Program::parse(&value_expression).map_err(|e| {
1621 KclErrorWithOutputs::no_outputs(KclError::refactor(format_kcl_error_message(
1622 "Invalid constraint value",
1623 &e,
1624 )))
1625 })?;
1626 if !errors.is_empty() {
1627 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1628 format_compilation_issues("Invalid constraint value", &errors),
1629 )));
1630 }
1631 let mut parsed = parsed.ok_or_else(|| {
1632 KclErrorWithOutputs::no_outputs(KclError::refactor("No AST produced from value expression".to_string()))
1633 })?;
1634 if parsed.ast.body.is_empty() {
1635 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1636 "Empty value expression".to_string(),
1637 )));
1638 }
1639 let first = parsed.ast.body.remove(0);
1640 let ast::BodyItem::ExpressionStatement(expr_stmt) = first else {
1641 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1642 "Value expression must be a simple expression".to_string(),
1643 )));
1644 };
1645
1646 let new_value: ast::BinaryPart = expr_stmt
1647 .inner
1648 .expression
1649 .try_into()
1650 .map_err(|e: String| KclErrorWithOutputs::no_outputs(KclError::refactor(e)))?;
1651
1652 self.mutate_ast(
1653 &mut new_ast,
1654 constraint_id,
1655 AstMutateCommand::EditConstraintValue { value: new_value },
1656 )
1657 .map_err(KclErrorWithOutputs::no_outputs)?;
1658
1659 self.execute_after_edit(
1660 ctx,
1661 sketch,
1662 sketch_block_ref,
1663 &mut new_ast,
1664 ExecuteAfterEditOptions {
1665 segment_ids_edited: Default::default(),
1666 edit_kind: EditDeleteKind::Edit,
1667 commit_solved_initial_guesses: true,
1668 },
1669 )
1670 .await
1671 }
1672
1673 async fn edit_distance_constraint_label_position(
1674 &mut self,
1675 ctx: &ExecutorContext,
1676 _version: Version,
1677 sketch: ObjectId,
1678 constraint_id: ObjectId,
1679 label_position: Point2d<Number>,
1680 anchor_segment_ids: Vec<ObjectId>,
1681 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1682 let sketch_block_ref =
1684 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1685
1686 let mut new_ast = self.program.ast.clone();
1687 self.mutate_constraint_label_position(&mut new_ast, constraint_id, label_position)
1688 .map_err(KclErrorWithOutputs::no_outputs)?;
1689 let commit_solved_initial_guesses = self.next_edit_commits_solver_solutions.take().unwrap_or(true);
1690
1691 self.execute_after_edit(
1692 ctx,
1693 sketch,
1694 sketch_block_ref,
1695 &mut new_ast,
1696 ExecuteAfterEditOptions {
1697 segment_ids_edited: anchor_segment_ids.into_iter().collect(),
1698 edit_kind: EditDeleteKind::Edit,
1699 commit_solved_initial_guesses,
1700 },
1701 )
1702 .await
1703 }
1704
1705 async fn batch_split_segment_operations(
1713 &mut self,
1714 ctx: &ExecutorContext,
1715 _version: Version,
1716 sketch: ObjectId,
1717 edit_segments: Vec<ExistingSegmentCtor>,
1718 add_constraints: Vec<Constraint>,
1719 delete_constraint_ids: Vec<ObjectId>,
1720 _new_segment_info: sketch::NewSegmentInfo,
1721 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1722 let sketch_block_ref =
1724 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1725
1726 let mut new_ast = self.program.ast.clone();
1727 let mut segment_ids_edited = AhashIndexSet::with_capacity_and_hasher(edit_segments.len(), Default::default());
1728
1729 for segment in edit_segments {
1731 segment_ids_edited.insert(segment.id);
1732 match segment.ctor {
1733 SegmentCtor::Point(ctor) => self
1734 .edit_point(&mut new_ast, sketch, segment.id, ctor)
1735 .map_err(KclErrorWithOutputs::no_outputs)?,
1736 SegmentCtor::Line(ctor) => self
1737 .edit_line(&mut new_ast, sketch, segment.id, ctor)
1738 .map_err(KclErrorWithOutputs::no_outputs)?,
1739 SegmentCtor::Arc(ctor) => self
1740 .edit_arc(&mut new_ast, sketch, segment.id, ctor)
1741 .map_err(KclErrorWithOutputs::no_outputs)?,
1742 SegmentCtor::Circle(ctor) => self
1743 .edit_circle(&mut new_ast, sketch, segment.id, ctor)
1744 .map_err(KclErrorWithOutputs::no_outputs)?,
1745 SegmentCtor::ControlPointSpline(ctor) => self
1746 .edit_control_point_spline(&mut new_ast, sketch, segment.id, ctor)
1747 .map_err(KclErrorWithOutputs::no_outputs)?,
1748 }
1749 }
1750
1751 for constraint in add_constraints {
1753 match constraint {
1754 Constraint::Coincident(coincident) => {
1755 self.add_coincident(sketch, coincident, &mut new_ast)
1756 .await
1757 .map_err(KclErrorWithOutputs::no_outputs)?;
1758 }
1759 Constraint::Distance(distance) => {
1760 self.add_distance(sketch, distance, &mut new_ast)
1761 .await
1762 .map_err(KclErrorWithOutputs::no_outputs)?;
1763 }
1764 Constraint::EqualRadius(equal_radius) => {
1765 self.add_equal_radius(sketch, equal_radius, &mut new_ast)
1766 .await
1767 .map_err(KclErrorWithOutputs::no_outputs)?;
1768 }
1769 Constraint::Fixed(fixed) => {
1770 self.add_fixed_constraints(sketch, fixed.points, &mut new_ast)
1771 .await
1772 .map_err(KclErrorWithOutputs::no_outputs)?;
1773 }
1774 Constraint::HorizontalDistance(distance) => {
1775 self.add_horizontal_distance(sketch, distance, &mut new_ast)
1776 .await
1777 .map_err(KclErrorWithOutputs::no_outputs)?;
1778 }
1779 Constraint::VerticalDistance(distance) => {
1780 self.add_vertical_distance(sketch, distance, &mut new_ast)
1781 .await
1782 .map_err(KclErrorWithOutputs::no_outputs)?;
1783 }
1784 Constraint::Horizontal(horizontal) => {
1785 self.add_horizontal(sketch, horizontal, &mut new_ast)
1786 .await
1787 .map_err(KclErrorWithOutputs::no_outputs)?;
1788 }
1789 Constraint::LinesEqualLength(lines_equal_length) => {
1790 self.add_lines_equal_length(sketch, lines_equal_length, &mut new_ast)
1791 .await
1792 .map_err(KclErrorWithOutputs::no_outputs)?;
1793 }
1794 Constraint::Midpoint(midpoint) => {
1795 self.add_midpoint(sketch, midpoint, &mut new_ast)
1796 .await
1797 .map_err(KclErrorWithOutputs::no_outputs)?;
1798 }
1799 Constraint::Parallel(parallel) => {
1800 self.add_parallel(sketch, parallel, &mut new_ast)
1801 .await
1802 .map_err(KclErrorWithOutputs::no_outputs)?;
1803 }
1804 Constraint::Perpendicular(perpendicular) => {
1805 self.add_perpendicular(sketch, perpendicular, &mut new_ast)
1806 .await
1807 .map_err(KclErrorWithOutputs::no_outputs)?;
1808 }
1809 Constraint::Vertical(vertical) => {
1810 self.add_vertical(sketch, vertical, &mut new_ast)
1811 .await
1812 .map_err(KclErrorWithOutputs::no_outputs)?;
1813 }
1814 Constraint::Diameter(diameter) => {
1815 self.add_diameter(sketch, diameter, &mut new_ast)
1816 .await
1817 .map_err(KclErrorWithOutputs::no_outputs)?;
1818 }
1819 Constraint::Radius(radius) => {
1820 self.add_radius(sketch, radius, &mut new_ast)
1821 .await
1822 .map_err(KclErrorWithOutputs::no_outputs)?;
1823 }
1824 Constraint::Symmetric(symmetric) => {
1825 self.add_symmetric(sketch, symmetric, &mut new_ast)
1826 .await
1827 .map_err(KclErrorWithOutputs::no_outputs)?;
1828 }
1829 Constraint::Angle(angle) => {
1830 self.add_angle(sketch, angle, &mut new_ast)
1831 .await
1832 .map_err(KclErrorWithOutputs::no_outputs)?;
1833 }
1834 Constraint::Tangent(tangent) => {
1835 self.add_tangent(sketch, tangent, &mut new_ast)
1836 .await
1837 .map_err(KclErrorWithOutputs::no_outputs)?;
1838 }
1839 }
1840 }
1841
1842 let constraint_ids_set = delete_constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
1844
1845 let has_constraint_deletions = !constraint_ids_set.is_empty();
1846 for constraint_id in constraint_ids_set {
1847 self.delete_constraint(&mut new_ast, sketch, constraint_id)
1848 .map_err(KclErrorWithOutputs::no_outputs)?;
1849 }
1850
1851 let (source_delta, mut scene_graph_delta) = self
1855 .execute_after_edit(
1856 ctx,
1857 sketch,
1858 sketch_block_ref,
1859 &mut new_ast,
1860 ExecuteAfterEditOptions {
1861 segment_ids_edited,
1862 edit_kind: EditDeleteKind::Edit,
1863 commit_solved_initial_guesses: true,
1864 },
1865 )
1866 .await?;
1867
1868 if has_constraint_deletions {
1871 scene_graph_delta.invalidates_ids = true;
1872 }
1873
1874 Ok((source_delta, scene_graph_delta))
1875 }
1876
1877 async fn batch_tail_cut_operations(
1878 &mut self,
1879 ctx: &ExecutorContext,
1880 _version: Version,
1881 sketch: ObjectId,
1882 edit_segments: Vec<ExistingSegmentCtor>,
1883 add_constraints: Vec<Constraint>,
1884 delete_constraint_ids: Vec<ObjectId>,
1885 additional_edited_segment_ids: Vec<ObjectId>,
1886 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1887 let sketch_block_ref =
1888 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1889
1890 let mut new_ast = self.program.ast.clone();
1891 let mut segment_ids_edited = AhashIndexSet::with_capacity_and_hasher(edit_segments.len(), Default::default());
1892
1893 for segment in edit_segments {
1895 segment_ids_edited.insert(segment.id);
1896 match segment.ctor {
1897 SegmentCtor::Point(ctor) => self
1898 .edit_point(&mut new_ast, sketch, segment.id, ctor)
1899 .map_err(KclErrorWithOutputs::no_outputs)?,
1900 SegmentCtor::Line(ctor) => self
1901 .edit_line(&mut new_ast, sketch, segment.id, ctor)
1902 .map_err(KclErrorWithOutputs::no_outputs)?,
1903 SegmentCtor::Arc(ctor) => self
1904 .edit_arc(&mut new_ast, sketch, segment.id, ctor)
1905 .map_err(KclErrorWithOutputs::no_outputs)?,
1906 SegmentCtor::Circle(ctor) => self
1907 .edit_circle(&mut new_ast, sketch, segment.id, ctor)
1908 .map_err(KclErrorWithOutputs::no_outputs)?,
1909 SegmentCtor::ControlPointSpline(ctor) => self
1910 .edit_control_point_spline(&mut new_ast, sketch, segment.id, ctor)
1911 .map_err(KclErrorWithOutputs::no_outputs)?,
1912 }
1913 }
1914
1915 segment_ids_edited.extend(additional_edited_segment_ids);
1916
1917 for constraint in add_constraints {
1919 match constraint {
1920 Constraint::Coincident(coincident) => {
1921 self.add_coincident(sketch, coincident, &mut new_ast)
1922 .await
1923 .map_err(KclErrorWithOutputs::no_outputs)?;
1924 }
1925 other => {
1926 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1927 "unsupported constraint in tail cut batch: {other:?}"
1928 ))));
1929 }
1930 }
1931 }
1932
1933 let constraint_ids_set = delete_constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
1935
1936 let has_constraint_deletions = !constraint_ids_set.is_empty();
1937 for constraint_id in constraint_ids_set {
1938 self.delete_constraint(&mut new_ast, sketch, constraint_id)
1939 .map_err(KclErrorWithOutputs::no_outputs)?;
1940 }
1941
1942 let (source_delta, mut scene_graph_delta) = self
1946 .execute_after_edit(
1947 ctx,
1948 sketch,
1949 sketch_block_ref,
1950 &mut new_ast,
1951 ExecuteAfterEditOptions {
1952 segment_ids_edited,
1953 edit_kind: EditDeleteKind::Edit,
1954 commit_solved_initial_guesses: true,
1955 },
1956 )
1957 .await?;
1958
1959 if has_constraint_deletions {
1962 scene_graph_delta.invalidates_ids = true;
1963 }
1964
1965 Ok((source_delta, scene_graph_delta))
1966 }
1967}
1968
1969impl FrontendState {
1970 pub async fn evaluate_expression(&self, ctx: &ExecutorContext, mut program: Program) -> ExecResult<ExecOutcome> {
1972 program.ast.inner_attrs.clone_from(&self.program.ast.inner_attrs);
1973 ctx.run_mock(&program, &MockConfig::default()).await
1974 }
1975
1976 pub async fn hack_set_program(&mut self, ctx: &ExecutorContext, program: Program) -> ExecResult<SetProgramOutcome> {
1977 self.program = program.clone();
1978
1979 self.point_freedom_cache.clear();
1990 match ctx.run_with_caching(program).await {
1991 Ok(outcome) => {
1992 let outcome = self.update_state_after_exec(outcome, true);
1993 let checkpoint_id = self
1994 .create_sketch_checkpoint(&ctx.cache, outcome.clone())
1995 .await
1996 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
1997 Ok(SetProgramOutcome::Success {
1998 scene_graph: Box::new(self.scene_graph_for_ui()),
1999 exec_outcome: Box::new(outcome),
2000 checkpoint_id: Some(checkpoint_id),
2001 })
2002 }
2003 Err(mut err) => {
2004 let outcome = self.exec_outcome_from_exec_error(err.clone())?;
2007 self.update_state_after_exec(outcome, true);
2008 err.scene_graph = Some(self.scene_graph_for_ui());
2009 Ok(SetProgramOutcome::ExecFailure { error: Box::new(err) })
2010 }
2011 }
2012 }
2013
2014 pub async fn engine_execute(
2017 &mut self,
2018 ctx: &ExecutorContext,
2019 program: Program,
2020 ) -> Result<SceneGraphDelta, KclErrorWithOutputs> {
2021 self.program = program.clone();
2022
2023 self.point_freedom_cache.clear();
2027 match ctx.run_with_caching(program).await {
2028 Ok(outcome) => {
2029 let outcome = self.update_state_after_exec(outcome, true);
2030 Ok(SceneGraphDelta {
2031 new_graph: self.scene_graph_for_ui(),
2032 exec_outcome: outcome,
2033 new_objects: Default::default(),
2035 invalidates_ids: Default::default(),
2037 })
2038 }
2039 Err(mut err) => {
2040 let outcome = self.exec_outcome_from_exec_error(err.clone())?;
2042 self.update_state_after_exec(outcome, true);
2043 err.scene_graph = Some(self.scene_graph_for_ui());
2044 Err(err)
2045 }
2046 }
2047 }
2048
2049 fn exec_outcome_from_exec_error(&self, err: KclErrorWithOutputs) -> Result<ExecOutcome, KclErrorWithOutputs> {
2050 if matches!(err.error, KclError::EngineHangup { .. }) {
2051 return Err(err);
2055 }
2056
2057 let KclErrorWithOutputs {
2058 error,
2059 mut non_fatal,
2060 variables,
2061 operations,
2062 artifact_graph,
2063 scene_objects,
2064 source_range_to_object,
2065 var_solutions,
2066 refactor_metadata,
2067 filenames,
2068 source_files,
2069 default_planes,
2070 ..
2071 } = err;
2072
2073 non_fatal.push(CompilationIssue::fatal(issue_source_range(&error), error.get_message()));
2074
2075 Ok(ExecOutcome {
2076 variables,
2077 #[cfg(test)]
2078 test_program_memory: Default::default(),
2079 filenames,
2080 from_cache: false,
2081 operations,
2082 artifact_graph,
2083 scene_objects,
2084 source_range_to_object,
2085 var_solutions,
2086 refactor_metadata,
2087 issues: non_fatal,
2088 source_files,
2089 default_planes,
2090 })
2091 }
2092
2093 async fn add_point(
2094 &mut self,
2095 ctx: &ExecutorContext,
2096 sketch: ObjectId,
2097 ctor: PointCtor,
2098 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2099 let at_ast = to_ast_point2d(&ctor.position)
2101 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2102 let point_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
2103 callee: ast::Node::no_src(ast_sketch2_name(POINT_FN)),
2104 unlabeled: None,
2105 arguments: vec![ast::LabeledArg {
2106 label: Some(ast::Identifier::new(POINT_AT_PARAM)),
2107 arg: at_ast,
2108 }],
2109 digest: None,
2110 non_code_meta: Default::default(),
2111 })));
2112
2113 let sketch_id = sketch;
2115 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2116 #[cfg(target_arch = "wasm32")]
2117 web_sys::console::error_1(
2118 &format!(
2119 "Sketch not found; sketch_id={sketch_id:?}, self.scene_graph.objects={:#?}",
2120 self.scene_graph.objects
2121 )
2122 .into(),
2123 );
2124 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2125 })?;
2126 let ObjectKind::Sketch(_) = &sketch_object.kind else {
2127 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2128 "Object is not a sketch, it is {}",
2129 sketch_object.kind.human_friendly_kind_with_article(),
2130 ))));
2131 };
2132 let mut new_ast = self.program.ast.clone();
2134 let (sketch_block_ref, _) = self
2135 .mutate_ast(
2136 &mut new_ast,
2137 sketch_id,
2138 AstMutateCommand::AddSketchBlockExprStmt { expr: point_ast },
2139 )
2140 .map_err(KclErrorWithOutputs::no_outputs)?;
2141 let new_source = source_from_ast(&new_ast);
2143 let new_program = parse_frontend_mutation_source(
2145 &new_source,
2146 "Error parsing KCL source after adding point",
2147 "No AST produced after adding point",
2148 )?;
2149
2150 let point_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2151 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2152 "Source range of point not found in sketch block: {sketch_block_ref:?}; {err:?}"
2153 )))
2154 })?;
2155
2156 self.program = new_program.clone();
2158
2159 let mut truncated_program = new_program;
2161 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2162 .map_err(KclErrorWithOutputs::no_outputs)?;
2163
2164 let outcome = ctx
2166 .run_mock(
2167 &truncated_program,
2168 &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2169 )
2170 .await?;
2171
2172 let new_object_ids = {
2173 let make_err =
2174 |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2175 let segment_id = outcome
2176 .source_range_to_object
2177 .get(&point_node_ref.range)
2178 .copied()
2179 .ok_or_else(|| make_err(format!("Source range of point not found: {point_node_ref:?}")))?;
2180 let segment_object = outcome
2181 .scene_objects
2182 .get(segment_id.0)
2183 .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2184 let ObjectKind::Segment { segment } = &segment_object.kind else {
2185 return Err(make_err(format!(
2186 "Object is not a segment, it is {}",
2187 segment_object.kind.human_friendly_kind_with_article()
2188 )));
2189 };
2190 let Segment::Point(_) = segment else {
2191 return Err(make_err(format!(
2192 "Segment is not a point, it is {}",
2193 segment.human_friendly_kind_with_article()
2194 )));
2195 };
2196 vec![segment_id]
2197 };
2198 let src_delta = SourceDelta { text: new_source };
2199 let outcome = self.update_state_after_exec(outcome, false);
2201 let scene_graph_delta = SceneGraphDelta {
2202 new_graph: self.scene_graph_for_ui(),
2203 invalidates_ids: false,
2204 new_objects: new_object_ids,
2205 exec_outcome: outcome,
2206 };
2207 Ok((src_delta, scene_graph_delta))
2208 }
2209
2210 async fn add_line(
2211 &mut self,
2212 ctx: &ExecutorContext,
2213 sketch: ObjectId,
2214 ctor: LineCtor,
2215 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2216 let start_ast = to_ast_point2d(&ctor.start)
2218 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2219 let end_ast = to_ast_point2d(&ctor.end)
2220 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2221 let mut arguments = vec![
2222 ast::LabeledArg {
2223 label: Some(ast::Identifier::new(LINE_START_PARAM)),
2224 arg: start_ast,
2225 },
2226 ast::LabeledArg {
2227 label: Some(ast::Identifier::new(LINE_END_PARAM)),
2228 arg: end_ast,
2229 },
2230 ];
2231 if ctor.construction == Some(true) {
2233 arguments.push(ast::LabeledArg {
2234 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2235 arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
2236 value: ast::LiteralValue::Bool(true),
2237 raw: "true".to_string(),
2238 digest: None,
2239 }))),
2240 });
2241 }
2242 let line_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
2243 callee: ast::Node::no_src(ast_sketch2_name(LINE_FN)),
2244 unlabeled: None,
2245 arguments,
2246 digest: None,
2247 non_code_meta: Default::default(),
2248 })));
2249
2250 let sketch_id = sketch;
2252 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2253 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2254 })?;
2255 let ObjectKind::Sketch(_) = &sketch_object.kind else {
2256 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2257 "Object is not a sketch, it is {}",
2258 sketch_object.kind.human_friendly_kind_with_article(),
2259 ))));
2260 };
2261 let mut new_ast = self.program.ast.clone();
2263 let (sketch_block_ref, _) = self
2264 .mutate_ast(
2265 &mut new_ast,
2266 sketch_id,
2267 AstMutateCommand::AddSketchBlockExprStmt { expr: line_ast },
2268 )
2269 .map_err(KclErrorWithOutputs::no_outputs)?;
2270 let new_source = source_from_ast(&new_ast);
2272 let new_program = parse_frontend_mutation_source(
2274 &new_source,
2275 "Error parsing KCL source after adding line",
2276 "No AST produced after adding line",
2277 )?;
2278
2279 let line_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2280 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2281 "Source range of line not found in sketch block: {sketch_block_ref:?}; {err:?}"
2282 )))
2283 })?;
2284
2285 self.program = new_program.clone();
2287
2288 let mut truncated_program = new_program;
2290 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2291 .map_err(KclErrorWithOutputs::no_outputs)?;
2292
2293 let outcome = ctx
2295 .run_mock(
2296 &truncated_program,
2297 &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2298 )
2299 .await?;
2300
2301 let new_object_ids = {
2302 let make_err =
2303 |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2304 let segment_id = outcome
2305 .source_range_to_object
2306 .get(&line_node_ref.range)
2307 .copied()
2308 .ok_or_else(|| make_err(format!("Source range of line not found: {line_node_ref:?}")))?;
2309 let segment_object = outcome
2310 .scene_object_by_id(segment_id)
2311 .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2312 let ObjectKind::Segment { segment } = &segment_object.kind else {
2313 return Err(make_err(format!(
2314 "Object is not a segment, it is {}",
2315 segment_object.kind.human_friendly_kind_with_article()
2316 )));
2317 };
2318 let Segment::Line(line) = segment else {
2319 return Err(make_err(format!(
2320 "Segment is not a line, it is {}",
2321 segment.human_friendly_kind_with_article()
2322 )));
2323 };
2324 vec![line.start, line.end, segment_id]
2325 };
2326 let src_delta = SourceDelta { text: new_source };
2327 let outcome = self.update_state_after_exec(outcome, false);
2329 let scene_graph_delta = SceneGraphDelta {
2330 new_graph: self.scene_graph_for_ui(),
2331 invalidates_ids: false,
2332 new_objects: new_object_ids,
2333 exec_outcome: outcome,
2334 };
2335 Ok((src_delta, scene_graph_delta))
2336 }
2337
2338 async fn add_arc(
2339 &mut self,
2340 ctx: &ExecutorContext,
2341 sketch: ObjectId,
2342 ctor: ArcCtor,
2343 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2344 let start_ast = to_ast_point2d(&ctor.start)
2346 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2347 let end_ast = to_ast_point2d(&ctor.end)
2348 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2349 let center_ast = to_ast_point2d(&ctor.center)
2350 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2351 let mut arguments = vec![
2352 ast::LabeledArg {
2353 label: Some(ast::Identifier::new(ARC_START_PARAM)),
2354 arg: start_ast,
2355 },
2356 ast::LabeledArg {
2357 label: Some(ast::Identifier::new(ARC_END_PARAM)),
2358 arg: end_ast,
2359 },
2360 ast::LabeledArg {
2361 label: Some(ast::Identifier::new(ARC_CENTER_PARAM)),
2362 arg: center_ast,
2363 },
2364 ];
2365 if ctor.direction == Some(ArcDirection::Cw) {
2368 arguments.push(ast::LabeledArg {
2369 label: Some(ast::Identifier::new(ARC_DIRECTION_PARAM)),
2370 arg: ast::Expr::Name(BoxNode::new(ast::Name::new(ARC_DIRECTION_CW_NAME))),
2371 });
2372 }
2373 if ctor.construction == Some(true) {
2375 arguments.push(ast::LabeledArg {
2376 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2377 arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
2378 value: ast::LiteralValue::Bool(true),
2379 raw: "true".to_string(),
2380 digest: None,
2381 }))),
2382 });
2383 }
2384 let arc_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
2385 callee: ast::Node::no_src(ast_sketch2_name(ARC_FN)),
2386 unlabeled: None,
2387 arguments,
2388 digest: None,
2389 non_code_meta: Default::default(),
2390 })));
2391
2392 let sketch_id = sketch;
2394 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2395 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2396 })?;
2397 let ObjectKind::Sketch(_) = &sketch_object.kind else {
2398 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2399 "Object is not a sketch, it is {}",
2400 sketch_object.kind.human_friendly_kind_with_article(),
2401 ))));
2402 };
2403 let mut new_ast = self.program.ast.clone();
2405 let (sketch_block_ref, _) = self
2406 .mutate_ast(
2407 &mut new_ast,
2408 sketch_id,
2409 AstMutateCommand::AddSketchBlockExprStmt { expr: arc_ast },
2410 )
2411 .map_err(KclErrorWithOutputs::no_outputs)?;
2412 let new_source = source_from_ast(&new_ast);
2414 let new_program = parse_frontend_mutation_source(
2416 &new_source,
2417 "Error parsing KCL source after adding arc",
2418 "No AST produced after adding arc",
2419 )?;
2420
2421 let arc_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2422 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2423 "Source range of arc not found in sketch block: {sketch_block_ref:?}; {err:?}"
2424 )))
2425 })?;
2426
2427 self.program = new_program.clone();
2429
2430 let mut truncated_program = new_program;
2432 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2433 .map_err(KclErrorWithOutputs::no_outputs)?;
2434
2435 let outcome = ctx
2437 .run_mock(
2438 &truncated_program,
2439 &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2440 )
2441 .await?;
2442
2443 let new_object_ids = {
2444 let make_err =
2445 |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2446 let segment_id = outcome
2447 .source_range_to_object
2448 .get(&arc_node_ref.range)
2449 .copied()
2450 .ok_or_else(|| make_err(format!("Source range of arc not found: {arc_node_ref:?}")))?;
2451 let segment_object = outcome
2452 .scene_objects
2453 .get(segment_id.0)
2454 .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2455 let ObjectKind::Segment { segment } = &segment_object.kind else {
2456 return Err(make_err(format!(
2457 "Object is not a segment, it is {}",
2458 segment_object.kind.human_friendly_kind_with_article()
2459 )));
2460 };
2461 let Segment::Arc(arc) = segment else {
2462 return Err(make_err(format!(
2463 "Segment is not an arc, it is {}",
2464 segment.human_friendly_kind_with_article()
2465 )));
2466 };
2467 vec![arc.start, arc.end, arc.center, segment_id]
2468 };
2469 let src_delta = SourceDelta { text: new_source };
2470 let outcome = self.update_state_after_exec(outcome, false);
2472 let scene_graph_delta = SceneGraphDelta {
2473 new_graph: self.scene_graph_for_ui(),
2474 invalidates_ids: false,
2475 new_objects: new_object_ids,
2476 exec_outcome: outcome,
2477 };
2478 Ok((src_delta, scene_graph_delta))
2479 }
2480
2481 async fn add_circle(
2482 &mut self,
2483 ctx: &ExecutorContext,
2484 sketch: ObjectId,
2485 ctor: CircleCtor,
2486 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2487 let start_ast = to_ast_point2d(&ctor.start)
2489 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2490 let center_ast = to_ast_point2d(&ctor.center)
2491 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2492 let mut arguments = vec![
2493 ast::LabeledArg {
2494 label: Some(ast::Identifier::new(CIRCLE_START_PARAM)),
2495 arg: start_ast,
2496 },
2497 ast::LabeledArg {
2498 label: Some(ast::Identifier::new(CIRCLE_CENTER_PARAM)),
2499 arg: center_ast,
2500 },
2501 ];
2502 if ctor.construction == Some(true) {
2504 arguments.push(ast::LabeledArg {
2505 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2506 arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
2507 value: ast::LiteralValue::Bool(true),
2508 raw: "true".to_string(),
2509 digest: None,
2510 }))),
2511 });
2512 }
2513 let circle_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
2514 callee: ast::Node::no_src(ast_sketch2_name(CIRCLE_FN)),
2515 unlabeled: None,
2516 arguments,
2517 digest: None,
2518 non_code_meta: Default::default(),
2519 })));
2520
2521 let sketch_id = sketch;
2523 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2524 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2525 })?;
2526 let ObjectKind::Sketch(_) = &sketch_object.kind else {
2527 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2528 "Object is not a sketch, it is {}",
2529 sketch_object.kind.human_friendly_kind_with_article(),
2530 ))));
2531 };
2532 let mut new_ast = self.program.ast.clone();
2534 let (sketch_block_ref, _) = self
2535 .mutate_ast(
2536 &mut new_ast,
2537 sketch_id,
2538 AstMutateCommand::AddSketchBlockVarDecl {
2539 prefix: CIRCLE_VARIABLE.to_owned(),
2540 expr: circle_ast,
2541 },
2542 )
2543 .map_err(KclErrorWithOutputs::no_outputs)?;
2544 let new_source = source_from_ast(&new_ast);
2546 let new_program = parse_frontend_mutation_source(
2548 &new_source,
2549 "Error parsing KCL source after adding circle",
2550 "No AST produced after adding circle",
2551 )?;
2552
2553 let circle_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2554 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2555 "Source range of circle not found in sketch block: {sketch_block_ref:?}; {err:?}"
2556 )))
2557 })?;
2558
2559 self.program = new_program.clone();
2561
2562 let mut truncated_program = new_program;
2564 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2565 .map_err(KclErrorWithOutputs::no_outputs)?;
2566
2567 let outcome = ctx
2569 .run_mock(
2570 &truncated_program,
2571 &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2572 )
2573 .await?;
2574
2575 let new_object_ids = {
2576 let make_err =
2577 |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2578 let segment_id = outcome
2579 .source_range_to_object
2580 .get(&circle_node_ref.range)
2581 .copied()
2582 .ok_or_else(|| make_err(format!("Source range of circle not found: {circle_node_ref:?}")))?;
2583 let segment_object = outcome
2584 .scene_objects
2585 .get(segment_id.0)
2586 .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2587 let ObjectKind::Segment { segment } = &segment_object.kind else {
2588 return Err(make_err(format!(
2589 "Object is not a segment, it is {}",
2590 segment_object.kind.human_friendly_kind_with_article()
2591 )));
2592 };
2593 let Segment::Circle(circle) = segment else {
2594 return Err(make_err(format!(
2595 "Segment is not a circle, it is {}",
2596 segment.human_friendly_kind_with_article()
2597 )));
2598 };
2599 vec![circle.start, circle.center, segment_id]
2600 };
2601 let src_delta = SourceDelta { text: new_source };
2602 let outcome = self.update_state_after_exec(outcome, false);
2604 let scene_graph_delta = SceneGraphDelta {
2605 new_graph: self.scene_graph_for_ui(),
2606 invalidates_ids: false,
2607 new_objects: new_object_ids,
2608 exec_outcome: outcome,
2609 };
2610 Ok((src_delta, scene_graph_delta))
2611 }
2612
2613 async fn add_control_point_spline(
2614 &mut self,
2615 ctx: &ExecutorContext,
2616 sketch: ObjectId,
2617 ctor: ControlPointSplineCtor,
2618 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2619 let new_program = ensure_control_point_spline_experimental_features(&self.program)
2620 .map_err(KclErrorWithOutputs::no_outputs)?;
2621
2622 let points_ast = to_ast_point2d_array(&ctor.points)
2623 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2624 let mut arguments = vec![ast::LabeledArg {
2625 label: Some(ast::Identifier::new(CONTROL_POINT_SPLINE_POINTS_PARAM)),
2626 arg: points_ast,
2627 }];
2628 if ctor.construction == Some(true) {
2629 arguments.push(ast::LabeledArg {
2630 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2631 arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
2632 value: ast::LiteralValue::Bool(true),
2633 raw: "true".to_string(),
2634 digest: None,
2635 }))),
2636 });
2637 }
2638 let spline_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
2639 callee: ast::Node::no_src(ast_sketch2_name(CONTROL_POINT_SPLINE_FN)),
2640 unlabeled: None,
2641 arguments,
2642 digest: None,
2643 non_code_meta: Default::default(),
2644 })));
2645
2646 let sketch_object = self.scene_graph.objects.get(sketch.0).ok_or_else(|| {
2647 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2648 })?;
2649 let ObjectKind::Sketch(_) = &sketch_object.kind else {
2650 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2651 "Object is not a sketch, it is {}",
2652 sketch_object.kind.human_friendly_kind_with_article(),
2653 ))));
2654 };
2655
2656 let mut new_ast = new_program.ast.clone();
2657 let (sketch_block_ref, _) = self
2658 .mutate_ast(
2659 &mut new_ast,
2660 sketch,
2661 AstMutateCommand::AddSketchBlockExprStmt { expr: spline_ast },
2662 )
2663 .map_err(KclErrorWithOutputs::no_outputs)?;
2664 let new_source = source_from_ast(&new_ast);
2665 let new_program = parse_frontend_mutation_source(
2666 &new_source,
2667 "Error parsing KCL source after adding controlPointSpline",
2668 "No AST produced after adding controlPointSpline",
2669 )?;
2670
2671 let spline_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2672 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2673 "Source range of controlPointSpline not found in sketch block: {sketch_block_ref:?}; {err:?}"
2674 )))
2675 })?;
2676
2677 self.program = new_program.clone();
2678
2679 let mut truncated_program = new_program;
2680 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2681 .map_err(KclErrorWithOutputs::no_outputs)?;
2682
2683 let outcome = ctx
2684 .run_mock(
2685 &truncated_program,
2686 &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2687 )
2688 .await?;
2689
2690 let new_object_ids = {
2691 let make_err =
2692 |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2693 let segment_id = outcome
2694 .source_range_to_object
2695 .get(&spline_node_ref.range)
2696 .copied()
2697 .ok_or_else(|| {
2698 make_err(format!(
2699 "Source range of controlPointSpline not found: {spline_node_ref:?}"
2700 ))
2701 })?;
2702 let segment_object = outcome
2703 .scene_objects
2704 .get(segment_id.0)
2705 .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2706 let ObjectKind::Segment { segment } = &segment_object.kind else {
2707 return Err(make_err(format!(
2708 "Object is not a segment, it is {}",
2709 segment_object.kind.human_friendly_kind_with_article()
2710 )));
2711 };
2712 let Segment::ControlPointSpline(spline) = segment else {
2713 return Err(make_err(format!(
2714 "Segment is not a control point spline, it is {}",
2715 segment.human_friendly_kind_with_article()
2716 )));
2717 };
2718
2719 let mut ids = outcome
2720 .scene_objects
2721 .iter()
2722 .filter_map(|obj| match &obj.kind {
2723 ObjectKind::Segment {
2724 segment: Segment::Line(line),
2725 } if line.owner == Some(segment_id) => Some(obj.id),
2726 _ => None,
2727 })
2728 .collect::<Vec<_>>();
2729 ids.extend(spline.controls.clone());
2730 ids.push(segment_id);
2731 ids
2732 };
2733 let src_delta = SourceDelta { text: new_source };
2734 let outcome = self.update_state_after_exec(outcome, false);
2735 let scene_graph_delta = SceneGraphDelta {
2736 new_graph: self.scene_graph_for_ui(),
2737 invalidates_ids: false,
2738 new_objects: new_object_ids,
2739 exec_outcome: outcome,
2740 };
2741 Ok((src_delta, scene_graph_delta))
2742 }
2743
2744 fn edit_point(
2745 &mut self,
2746 new_ast: &mut ast::Node<ast::Program>,
2747 sketch: ObjectId,
2748 point: ObjectId,
2749 ctor: PointCtor,
2750 ) -> Result<(), KclError> {
2751 let new_at_ast = to_ast_point2d(&ctor.position).map_err(|err| KclError::refactor(err.to_string()))?;
2753
2754 let sketch_id = sketch;
2756 let sketch_object = self
2757 .scene_graph
2758 .objects
2759 .get(sketch_id.0)
2760 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2761 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2762 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2763 };
2764 sketch.segments.iter().find(|o| **o == point).ok_or_else(|| {
2765 KclError::refactor(format!("Point not found in sketch: point={point:?}, sketch={sketch:?}"))
2766 })?;
2767 let point_id = point;
2769 let point_object = self
2770 .scene_graph
2771 .objects
2772 .get(point_id.0)
2773 .ok_or_else(|| KclError::refactor(format!("Point not found in scene graph: point={point:?}")))?;
2774 let ObjectKind::Segment {
2775 segment: Segment::Point(point),
2776 } = &point_object.kind
2777 else {
2778 return Err(KclError::refactor(format!(
2779 "Object is not a point segment: {point_object:?}"
2780 )));
2781 };
2782
2783 if let Some(owner_id) = point.owner {
2785 let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
2786 KclError::refactor(format!(
2787 "Internal: Owner of point not found in scene graph: owner={owner_id:?}",
2788 ))
2789 })?;
2790 let ObjectKind::Segment { segment } = &owner_object.kind else {
2791 return Err(KclError::refactor(format!(
2792 "Internal: Owner of point is not a segment, but found {}",
2793 owner_object.kind.human_friendly_kind_with_article()
2794 )));
2795 };
2796
2797 if let Segment::Line(line) = segment {
2799 let SegmentCtor::Line(line_ctor) = &line.ctor else {
2800 return Err(KclError::refactor(format!(
2801 "Internal: Owner of point does not have line ctor, but found {}",
2802 line.ctor.human_friendly_kind_with_article()
2803 )));
2804 };
2805 let mut line_ctor = line_ctor.clone();
2806 if line.start == point_id {
2808 line_ctor.start = ctor.position;
2809 } else if line.end == point_id {
2810 line_ctor.end = ctor.position;
2811 } else {
2812 return Err(KclError::refactor(format!(
2813 "Internal: Point is not part of owner's line segment: point={point_id:?}, line={owner_id:?}"
2814 )));
2815 }
2816 return self.edit_line(new_ast, sketch_id, owner_id, line_ctor);
2817 }
2818
2819 if let Segment::Arc(arc) = segment {
2821 let SegmentCtor::Arc(arc_ctor) = &arc.ctor else {
2822 return Err(KclError::refactor(format!(
2823 "Internal: Owner of point does not have arc ctor, but found {}",
2824 arc.ctor.human_friendly_kind_with_article()
2825 )));
2826 };
2827 let mut arc_ctor = arc_ctor.clone();
2828 if arc.center == point_id {
2830 arc_ctor.center = ctor.position;
2831 } else if arc.start == point_id {
2832 arc_ctor.start = ctor.position;
2833 } else if arc.end == point_id {
2834 arc_ctor.end = ctor.position;
2835 } else {
2836 return Err(KclError::refactor(format!(
2837 "Internal: Point is not part of owner's arc segment: point={point_id:?}, arc={owner_id:?}"
2838 )));
2839 }
2840 return self.edit_arc(new_ast, sketch_id, owner_id, arc_ctor);
2841 }
2842
2843 if let Segment::Circle(circle) = segment {
2845 let SegmentCtor::Circle(circle_ctor) = &circle.ctor else {
2846 return Err(KclError::refactor(format!(
2847 "Internal: Owner of point does not have circle ctor, but found {}",
2848 circle.ctor.human_friendly_kind_with_article()
2849 )));
2850 };
2851 let mut circle_ctor = circle_ctor.clone();
2852 if circle.center == point_id {
2853 circle_ctor.center = ctor.position;
2854 } else if circle.start == point_id {
2855 circle_ctor.start = ctor.position;
2856 } else {
2857 return Err(KclError::refactor(format!(
2858 "Internal: Point is not part of owner's circle segment: point={point_id:?}, circle={owner_id:?}"
2859 )));
2860 }
2861 return self.edit_circle(new_ast, sketch_id, owner_id, circle_ctor);
2862 }
2863
2864 if let Segment::ControlPointSpline(spline) = segment {
2865 let SegmentCtor::ControlPointSpline(spline_ctor) = &spline.ctor else {
2866 return Err(KclError::refactor(format!(
2867 "Internal: Owner of point does not have controlPointSpline ctor, but found {}",
2868 spline.ctor.human_friendly_kind_with_article()
2869 )));
2870 };
2871 let mut spline_ctor = spline_ctor.clone();
2872 let Some(control_index) = spline.controls.iter().position(|id| *id == point_id) else {
2873 return Err(KclError::refactor(format!(
2874 "Internal: Point is not part of owner's controlPointSpline segment: point={point_id:?}, spline={owner_id:?}"
2875 )));
2876 };
2877 spline_ctor.points[control_index] = ctor.position;
2878 return self.edit_control_point_spline(new_ast, sketch_id, owner_id, spline_ctor);
2879 }
2880
2881 }
2884
2885 self.mutate_ast(new_ast, point_id, AstMutateCommand::EditPoint { at: new_at_ast })?;
2887 Ok(())
2888 }
2889
2890 fn edit_line(
2891 &mut self,
2892 new_ast: &mut ast::Node<ast::Program>,
2893 sketch: ObjectId,
2894 line: ObjectId,
2895 ctor: LineCtor,
2896 ) -> Result<(), KclError> {
2897 let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2899 let new_end_ast = to_ast_point2d(&ctor.end).map_err(|err| KclError::refactor(err.to_string()))?;
2900
2901 let sketch_id = sketch;
2903 let sketch_object = self
2904 .scene_graph
2905 .objects
2906 .get(sketch_id.0)
2907 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2908 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2909 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2910 };
2911 sketch
2912 .segments
2913 .iter()
2914 .find(|o| **o == line)
2915 .ok_or_else(|| KclError::refactor(format!("Line not found in sketch: line={line:?}, sketch={sketch:?}")))?;
2916 let line_id = line;
2918 let line_object = self
2919 .scene_graph
2920 .objects
2921 .get(line_id.0)
2922 .ok_or_else(|| KclError::refactor(format!("Line not found in scene graph: line={line:?}")))?;
2923 let ObjectKind::Segment { .. } = &line_object.kind else {
2924 let kind = line_object.kind.human_friendly_kind_with_article();
2925 return Err(KclError::refactor(format!(
2926 "This constraint only works on Segments, but you selected {kind}"
2927 )));
2928 };
2929
2930 self.mutate_ast(
2932 new_ast,
2933 line_id,
2934 AstMutateCommand::EditLine {
2935 start: new_start_ast,
2936 end: new_end_ast,
2937 construction: ctor.construction,
2938 },
2939 )?;
2940 Ok(())
2941 }
2942
2943 fn edit_arc(
2944 &mut self,
2945 new_ast: &mut ast::Node<ast::Program>,
2946 sketch: ObjectId,
2947 arc: ObjectId,
2948 ctor: ArcCtor,
2949 ) -> Result<(), KclError> {
2950 let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2952 let new_end_ast = to_ast_point2d(&ctor.end).map_err(|err| KclError::refactor(err.to_string()))?;
2953 let new_center_ast = to_ast_point2d(&ctor.center).map_err(|err| KclError::refactor(err.to_string()))?;
2954
2955 let sketch_id = sketch;
2957 let sketch_object = self
2958 .scene_graph
2959 .objects
2960 .get(sketch_id.0)
2961 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2962 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2963 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2964 };
2965 sketch
2966 .segments
2967 .iter()
2968 .find(|o| **o == arc)
2969 .ok_or_else(|| KclError::refactor(format!("Arc not found in sketch: arc={arc:?}, sketch={sketch:?}")))?;
2970 let arc_id = arc;
2972 let arc_object = self
2973 .scene_graph
2974 .objects
2975 .get(arc_id.0)
2976 .ok_or_else(|| KclError::refactor(format!("Arc not found in scene graph: arc={arc:?}")))?;
2977 let ObjectKind::Segment { .. } = &arc_object.kind else {
2978 return Err(KclError::refactor(format!("Object is not a segment: {arc_object:?}")));
2979 };
2980
2981 self.mutate_ast(
2983 new_ast,
2984 arc_id,
2985 AstMutateCommand::EditArc {
2986 start: new_start_ast,
2987 end: new_end_ast,
2988 center: new_center_ast,
2989 direction: ctor.direction,
2990 construction: ctor.construction,
2991 },
2992 )?;
2993 Ok(())
2994 }
2995
2996 fn edit_circle(
2997 &mut self,
2998 new_ast: &mut ast::Node<ast::Program>,
2999 sketch: ObjectId,
3000 circle: ObjectId,
3001 ctor: CircleCtor,
3002 ) -> Result<(), KclError> {
3003 let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
3005 let new_center_ast = to_ast_point2d(&ctor.center).map_err(|err| KclError::refactor(err.to_string()))?;
3006
3007 let sketch_id = sketch;
3009 let sketch_object = self
3010 .scene_graph
3011 .objects
3012 .get(sketch_id.0)
3013 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
3014 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
3015 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
3016 };
3017 sketch.segments.iter().find(|o| **o == circle).ok_or_else(|| {
3018 KclError::refactor(format!(
3019 "Circle not found in sketch: circle={circle:?}, sketch={sketch:?}"
3020 ))
3021 })?;
3022 let circle_id = circle;
3024 let circle_object = self
3025 .scene_graph
3026 .objects
3027 .get(circle_id.0)
3028 .ok_or_else(|| KclError::refactor(format!("Circle not found in scene graph: circle={circle:?}")))?;
3029 let ObjectKind::Segment { .. } = &circle_object.kind else {
3030 return Err(KclError::refactor(format!(
3031 "Object is not a segment: {circle_object:?}"
3032 )));
3033 };
3034
3035 self.mutate_ast(
3037 new_ast,
3038 circle_id,
3039 AstMutateCommand::EditCircle {
3040 start: new_start_ast,
3041 center: new_center_ast,
3042 construction: ctor.construction,
3043 },
3044 )?;
3045 Ok(())
3046 }
3047
3048 fn edit_control_point_spline(
3049 &mut self,
3050 new_ast: &mut ast::Node<ast::Program>,
3051 sketch: ObjectId,
3052 spline: ObjectId,
3053 ctor: ControlPointSplineCtor,
3054 ) -> Result<(), KclError> {
3055 let points_ast = to_ast_point2d_array(&ctor.points).map_err(|err| KclError::refactor(err.to_string()))?;
3056
3057 let sketch_object = self
3058 .scene_graph
3059 .objects
3060 .get(sketch.0)
3061 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
3062 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
3063 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
3064 };
3065 sketch.segments.iter().find(|o| **o == spline).ok_or_else(|| {
3066 KclError::refactor(format!(
3067 "Control point spline not found in sketch: spline={spline:?}, sketch={sketch:?}"
3068 ))
3069 })?;
3070
3071 let spline_object =
3072 self.scene_graph.objects.get(spline.0).ok_or_else(|| {
3073 KclError::refactor(format!("Control point spline not found in scene graph: {spline:?}"))
3074 })?;
3075 let ObjectKind::Segment { .. } = &spline_object.kind else {
3076 return Err(KclError::refactor(format!(
3077 "Object is not a segment: {spline_object:?}"
3078 )));
3079 };
3080
3081 self.mutate_ast(
3082 new_ast,
3083 spline,
3084 AstMutateCommand::EditControlPointSpline {
3085 points: points_ast,
3086 construction: ctor.construction,
3087 },
3088 )?;
3089 Ok(())
3090 }
3091
3092 fn delete_segment(
3093 &mut self,
3094 new_ast: &mut ast::Node<ast::Program>,
3095 sketch: ObjectId,
3096 segment_id: ObjectId,
3097 ) -> Result<(), KclError> {
3098 let sketch_id = sketch;
3100 let sketch_object = self
3101 .scene_graph
3102 .objects
3103 .get(sketch_id.0)
3104 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
3105 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
3106 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
3107 };
3108 sketch.segments.iter().find(|o| **o == segment_id).ok_or_else(|| {
3109 KclError::refactor(format!(
3110 "Segment not found in sketch: segment={segment_id:?}, sketch={sketch:?}"
3111 ))
3112 })?;
3113 let segment_object =
3115 self.scene_graph.objects.get(segment_id.0).ok_or_else(|| {
3116 KclError::refactor(format!("Segment not found in scene graph: segment={segment_id:?}"))
3117 })?;
3118 let ObjectKind::Segment { .. } = &segment_object.kind else {
3119 return Err(KclError::refactor(format!(
3120 "Object is not a segment, it is {}",
3121 segment_object.kind.human_friendly_kind_with_article()
3122 )));
3123 };
3124
3125 self.mutate_ast(new_ast, segment_id, AstMutateCommand::DeleteNode)?;
3127 Ok(())
3128 }
3129
3130 fn delete_constraint(
3131 &mut self,
3132 new_ast: &mut ast::Node<ast::Program>,
3133 sketch: ObjectId,
3134 constraint_id: ObjectId,
3135 ) -> Result<(), KclError> {
3136 let sketch_id = sketch;
3138 let sketch_object = self
3139 .scene_graph
3140 .objects
3141 .get(sketch_id.0)
3142 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
3143 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
3144 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
3145 };
3146 sketch
3147 .constraints
3148 .iter()
3149 .find(|o| **o == constraint_id)
3150 .ok_or_else(|| {
3151 KclError::refactor(format!(
3152 "Constraint not found in sketch: constraint={constraint_id:?}, sketch={sketch:?}"
3153 ))
3154 })?;
3155 let constraint_object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
3157 KclError::refactor(format!(
3158 "Constraint not found in scene graph: constraint={constraint_id:?}"
3159 ))
3160 })?;
3161 let ObjectKind::Constraint { .. } = &constraint_object.kind else {
3162 return Err(KclError::refactor(format!(
3163 "Object is not a constraint, it is {}",
3164 constraint_object.kind.human_friendly_kind_with_article()
3165 )));
3166 };
3167
3168 self.mutate_ast(new_ast, constraint_id, AstMutateCommand::DeleteNode)?;
3170 Ok(())
3171 }
3172
3173 fn edit_coincident_constraint(
3174 &mut self,
3175 new_ast: &mut ast::Node<ast::Program>,
3176 constraint_id: ObjectId,
3177 segments: Vec<ConstraintSegment>,
3178 ) -> Result<(), KclError> {
3179 if segments.len() < 2 {
3180 return Err(KclError::refactor(format!(
3181 "Coincident constraint must have at least 2 inputs, got {}",
3182 segments.len()
3183 )));
3184 }
3185
3186 let segment_asts = segments
3187 .iter()
3188 .map(|segment| self.coincident_segment_to_ast(segment, new_ast))
3189 .collect::<Result<Vec<_>, _>>()?;
3190
3191 let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
3192 elements: segment_asts,
3193 digest: None,
3194 non_code_meta: Default::default(),
3195 })));
3196
3197 self.mutate_ast(
3198 new_ast,
3199 constraint_id,
3200 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3201 )?;
3202 Ok(())
3203 }
3204
3205 fn edit_horizontal_points_constraint(
3206 &mut self,
3207 new_ast: &mut ast::Node<ast::Program>,
3208 constraint_id: ObjectId,
3209 points: Vec<ConstraintSegment>,
3210 ) -> Result<(), KclError> {
3211 self.edit_axis_points_constraint(new_ast, constraint_id, points, "Horizontal")
3212 }
3213
3214 fn edit_vertical_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, "Vertical")
3221 }
3222
3223 fn edit_axis_points_constraint(
3224 &mut self,
3225 new_ast: &mut ast::Node<ast::Program>,
3226 constraint_id: ObjectId,
3227 points: Vec<ConstraintSegment>,
3228 constraint_name: &str,
3229 ) -> Result<(), KclError> {
3230 if points.len() < 2 {
3231 return Err(KclError::refactor(format!(
3232 "{constraint_name} points constraint must have at least 2 points, got {}",
3233 points.len()
3234 )));
3235 }
3236
3237 let point_asts = points
3238 .iter()
3239 .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
3240 .collect::<Result<Vec<_>, _>>()?;
3241
3242 let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
3243 elements: point_asts,
3244 digest: None,
3245 non_code_meta: Default::default(),
3246 })));
3247
3248 self.mutate_ast(
3249 new_ast,
3250 constraint_id,
3251 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3252 )?;
3253 Ok(())
3254 }
3255
3256 fn edit_equal_length_constraint(
3258 &mut self,
3259 new_ast: &mut ast::Node<ast::Program>,
3260 constraint_id: ObjectId,
3261 lines: Vec<ObjectId>,
3262 ) -> Result<(), KclError> {
3263 if lines.len() < 2 {
3264 return Err(KclError::refactor(format!(
3265 "Lines equal length constraint must have at least 2 lines, got {}",
3266 lines.len()
3267 )));
3268 }
3269
3270 let line_asts = lines
3271 .iter()
3272 .map(|line_id| {
3273 let line_object = self
3274 .scene_graph
3275 .objects
3276 .get(line_id.0)
3277 .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3278 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3279 let kind = line_object.kind.human_friendly_kind_with_article();
3280 return Err(KclError::refactor(format!(
3281 "This constraint only works on Segments, but you selected {kind}"
3282 )));
3283 };
3284 let Segment::Line(_) = line_segment else {
3285 let kind = line_segment.human_friendly_kind_with_article();
3286 return Err(KclError::refactor(format!(
3287 "Only lines can be made equal length, but you selected {kind}"
3288 )));
3289 };
3290
3291 get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)
3292 })
3293 .collect::<Result<Vec<_>, _>>()?;
3294
3295 let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
3296 elements: line_asts,
3297 digest: None,
3298 non_code_meta: Default::default(),
3299 })));
3300
3301 self.mutate_ast(
3302 new_ast,
3303 constraint_id,
3304 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3305 )?;
3306 Ok(())
3307 }
3308
3309 fn edit_parallel_constraint(
3311 &mut self,
3312 new_ast: &mut ast::Node<ast::Program>,
3313 constraint_id: ObjectId,
3314 lines: Vec<ObjectId>,
3315 ) -> Result<(), KclError> {
3316 if lines.len() < 2 {
3317 return Err(KclError::refactor(format!(
3318 "Parallel constraint must have at least 2 lines, got {}",
3319 lines.len()
3320 )));
3321 }
3322
3323 let line_asts = lines
3324 .iter()
3325 .map(|line_id| {
3326 let line_object = self
3327 .scene_graph
3328 .objects
3329 .get(line_id.0)
3330 .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3331 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3332 let kind = line_object.kind.human_friendly_kind_with_article();
3333 return Err(KclError::refactor(format!(
3334 "This constraint only works on Segments, but you selected {kind}"
3335 )));
3336 };
3337 let Segment::Line(_) = line_segment else {
3338 let kind = line_segment.human_friendly_kind_with_article();
3339 return Err(KclError::refactor(format!(
3340 "Only lines can be made parallel, but you selected {kind}"
3341 )));
3342 };
3343
3344 get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)
3345 })
3346 .collect::<Result<Vec<_>, _>>()?;
3347
3348 let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
3349 elements: line_asts,
3350 digest: None,
3351 non_code_meta: Default::default(),
3352 })));
3353
3354 self.mutate_ast(
3355 new_ast,
3356 constraint_id,
3357 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3358 )?;
3359 Ok(())
3360 }
3361
3362 fn edit_equal_radius_constraint(
3364 &mut self,
3365 new_ast: &mut ast::Node<ast::Program>,
3366 constraint_id: ObjectId,
3367 input: Vec<ObjectId>,
3368 ) -> Result<(), KclError> {
3369 if input.len() < 2 {
3370 return Err(KclError::refactor(format!(
3371 "equalRadius constraint must have at least 2 segments, got {}",
3372 input.len()
3373 )));
3374 }
3375
3376 let input_asts = input
3377 .iter()
3378 .map(|segment_id| self.equal_radius_segment_id_to_ast_reference(*segment_id, new_ast))
3379 .collect::<Result<Vec<_>, _>>()?;
3380
3381 let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
3382 elements: input_asts,
3383 digest: None,
3384 non_code_meta: Default::default(),
3385 })));
3386
3387 self.mutate_ast(
3388 new_ast,
3389 constraint_id,
3390 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3391 )?;
3392 Ok(())
3393 }
3394
3395 async fn execute_after_edit(
3396 &mut self,
3397 ctx: &ExecutorContext,
3398 sketch: ObjectId,
3399 sketch_block_ref: AstNodeRef,
3400 new_ast: &mut ast::Node<ast::Program>,
3401 options: ExecuteAfterEditOptions,
3402 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
3403 let ExecuteAfterEditOptions {
3404 segment_ids_edited,
3405 edit_kind,
3406 commit_solved_initial_guesses,
3407 } = options;
3408
3409 let new_source = source_from_ast(new_ast);
3411 let new_program = parse_frontend_mutation_source(
3413 &new_source,
3414 "Error parsing KCL source after editing",
3415 "No AST produced after editing",
3416 )?;
3417
3418 let is_delete = edit_kind.is_delete();
3420 let truncated_program = {
3421 let mut truncated_program = new_program.clone();
3422 only_sketch_block(
3423 &mut truncated_program.ast,
3424 &sketch_block_ref,
3425 edit_kind.to_change_kind(),
3426 )
3427 .map_err(KclErrorWithOutputs::no_outputs)?;
3428 truncated_program
3429 };
3430
3431 let drag_anchors = self.next_segment_drag_anchors.take().unwrap_or_default();
3433 let mock_config = MockConfig {
3434 sketch_block_id: Some(sketch),
3435 freedom_analysis: is_delete,
3436 segment_ids_edited: segment_ids_edited.clone(),
3437 drag_anchors,
3438 ..Default::default()
3439 };
3440 let outcome = ctx.run_mock(&truncated_program, &mock_config).await?;
3441
3442 self.program = new_program;
3444
3445 let outcome = self.update_state_after_exec(outcome, is_delete);
3447
3448 let src_delta = if commit_solved_initial_guesses {
3449 self.commit_var_solutions_to_program(&outcome, "editing")?
3450 } else {
3451 SourceDelta { text: new_source }
3452 };
3453 let scene_graph_delta = SceneGraphDelta {
3454 new_graph: self.scene_graph_for_ui(),
3455 invalidates_ids: is_delete,
3456 new_objects: Vec::new(),
3457 exec_outcome: outcome,
3458 };
3459 Ok((src_delta, scene_graph_delta))
3460 }
3461
3462 async fn execute_after_delete_sketch(
3463 &mut self,
3464 ctx: &ExecutorContext,
3465 new_ast: &mut ast::Node<ast::Program>,
3466 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
3467 let new_source = source_from_ast(new_ast);
3469 let new_program = parse_frontend_mutation_source(
3471 &new_source,
3472 "Error parsing KCL source after editing",
3473 "No AST produced after editing",
3474 )?;
3475
3476 self.program = new_program.clone();
3478
3479 let outcome = ctx.run_with_caching(new_program).await?;
3485 let freedom_analysis_ran = true;
3486
3487 let outcome = self.update_state_after_exec(outcome, freedom_analysis_ran);
3488
3489 let src_delta = SourceDelta { text: new_source };
3490 let scene_graph_delta = SceneGraphDelta {
3491 new_graph: self.scene_graph_for_ui(),
3492 invalidates_ids: true,
3493 new_objects: Vec::new(),
3494 exec_outcome: outcome,
3495 };
3496 Ok((src_delta, scene_graph_delta))
3497 }
3498
3499 fn point_id_to_ast_reference(
3504 &self,
3505 point_id: ObjectId,
3506 new_ast: &mut ast::Node<ast::Program>,
3507 ) -> Result<ast::Expr, KclError> {
3508 let point_object = self
3509 .scene_graph
3510 .objects
3511 .get(point_id.0)
3512 .ok_or_else(|| KclError::refactor(format!("Point not found: {point_id:?}")))?;
3513 let ObjectKind::Segment { segment: point_segment } = &point_object.kind else {
3514 return Err(KclError::refactor(format!("Object is not a segment: {point_object:?}")));
3515 };
3516 let Segment::Point(point) = point_segment else {
3517 return Err(KclError::refactor(format!(
3518 "Only points are currently supported: {point_object:?}"
3519 )));
3520 };
3521
3522 if let Some(owner_id) = point.owner {
3523 let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
3524 KclError::refactor(format!(
3525 "Owner of point not found in scene graph: point={point_id:?}, owner={owner_id:?}"
3526 ))
3527 })?;
3528 let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind else {
3529 return Err(KclError::refactor(format!(
3530 "Owner of point is not a segment, but found {}",
3531 owner_object.kind.human_friendly_kind_with_article()
3532 )));
3533 };
3534
3535 match owner_segment {
3536 Segment::Line(line) => {
3537 let property = if line.start == point_id {
3538 LINE_PROPERTY_START
3539 } else if line.end == point_id {
3540 LINE_PROPERTY_END
3541 } else {
3542 return Err(KclError::refactor(format!(
3543 "Internal: Point is not part of owner's line segment: point={point_id:?}, line={owner_id:?}"
3544 )));
3545 };
3546 get_or_insert_ast_reference(new_ast, &owner_object.source, LINE_VARIABLE, Some(property))
3547 }
3548 Segment::Arc(arc) => {
3549 let property = if arc.start == point_id {
3550 ARC_PROPERTY_START
3551 } else if arc.end == point_id {
3552 ARC_PROPERTY_END
3553 } else if arc.center == point_id {
3554 ARC_PROPERTY_CENTER
3555 } else {
3556 return Err(KclError::refactor(format!(
3557 "Internal: Point is not part of owner's arc segment: point={point_id:?}, arc={owner_id:?}"
3558 )));
3559 };
3560 get_or_insert_ast_reference(new_ast, &owner_object.source, ARC_VARIABLE, Some(property))
3561 }
3562 Segment::Circle(circle) => {
3563 let property = if circle.start == point_id {
3564 CIRCLE_PROPERTY_START
3565 } else if circle.center == point_id {
3566 CIRCLE_PROPERTY_CENTER
3567 } else {
3568 return Err(KclError::refactor(format!(
3569 "Internal: Point is not part of owner's circle segment: point={point_id:?}, circle={owner_id:?}"
3570 )));
3571 };
3572 get_or_insert_ast_reference(new_ast, &owner_object.source, CIRCLE_VARIABLE, Some(property))
3573 }
3574 Segment::ControlPointSpline(spline) => {
3575 let Some(index) = spline.controls.iter().position(|id| *id == point_id) else {
3576 return Err(KclError::refactor(format!(
3577 "Internal: Point is not part of owner's controlPointSpline segment: point={point_id:?}, spline={owner_id:?}"
3578 )));
3579 };
3580 let owner_expr =
3581 get_or_insert_ast_reference(new_ast, &owner_object.source, CONTROL_POINT_SPLINE_FN, None)?;
3582 let controls_expr = create_member_expression(owner_expr, CONTROL_POINT_SPLINE_PROPERTY_CONTROLS);
3583 Ok(create_index_expression(controls_expr, index))
3584 }
3585 _ => Err(KclError::refactor(format!(
3586 "Internal: Owner of point is not a supported segment type for constraints: {owner_segment:?}"
3587 ))),
3588 }
3589 } else {
3590 get_or_insert_ast_reference(new_ast, &point_object.source, "point", None)
3592 }
3593 }
3594
3595 fn line_id_to_ast_reference(
3596 &self,
3597 line_id: ObjectId,
3598 new_ast: &mut ast::Node<ast::Program>,
3599 ) -> Result<ast::Expr, KclError> {
3600 let line_object = self
3601 .scene_graph
3602 .objects
3603 .get(line_id.0)
3604 .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3605 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3606 return Err(KclError::refactor(format!("Object is not a segment: {line_object:?}")));
3607 };
3608 let Segment::Line(line) = line_segment else {
3609 return Err(KclError::refactor(format!(
3610 "Only lines are currently supported: {line_object:?}"
3611 )));
3612 };
3613
3614 if let Some(owner_id) = line.owner {
3615 let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
3616 KclError::refactor(format!(
3617 "Owner of line not found in scene graph: line={line_id:?}, owner={owner_id:?}"
3618 ))
3619 })?;
3620 let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind else {
3621 return Err(KclError::refactor(format!(
3622 "Owner of line is not a segment, but found {}",
3623 owner_object.kind.human_friendly_kind_with_article()
3624 )));
3625 };
3626
3627 match owner_segment {
3628 Segment::ControlPointSpline(spline) => {
3629 let Some(index) = spline
3630 .controls
3631 .windows(2)
3632 .position(|window| window[0] == line.start && window[1] == line.end)
3633 else {
3634 return Err(KclError::refactor(format!(
3635 "Internal: Line is not part of owner's controlPointSpline segment: line={line_id:?}, spline={owner_id:?}"
3636 )));
3637 };
3638 let owner_expr =
3639 get_or_insert_ast_reference(new_ast, &owner_object.source, CONTROL_POINT_SPLINE_FN, None)?;
3640 let edges_expr = create_member_expression(owner_expr, CONTROL_POINT_SPLINE_PROPERTY_EDGES);
3641 Ok(create_index_expression(edges_expr, index))
3642 }
3643 _ => Err(KclError::refactor(format!(
3644 "Internal: Owner of line is not a supported segment type for constraints: {owner_segment:?}"
3645 ))),
3646 }
3647 } else {
3648 get_or_insert_ast_reference(new_ast, &line_object.source, "line", None)
3649 }
3650 }
3651
3652 fn coincident_segment_to_ast(
3653 &self,
3654 segment: &ConstraintSegment,
3655 new_ast: &mut ast::Node<ast::Program>,
3656 ) -> Result<ast::Expr, KclError> {
3657 match segment {
3658 ConstraintSegment::Origin(_) => Ok(ast_name_expr("ORIGIN".to_owned())),
3659 ConstraintSegment::Segment(segment_id) => self.segment_id_to_constraint_ast_reference(*segment_id, new_ast),
3660 }
3661 }
3662
3663 fn segment_id_to_constraint_ast_reference(
3664 &self,
3665 segment_id: ObjectId,
3666 new_ast: &mut ast::Node<ast::Program>,
3667 ) -> Result<ast::Expr, KclError> {
3668 let segment_object = self
3669 .scene_graph
3670 .objects
3671 .get(segment_id.0)
3672 .ok_or_else(|| KclError::refactor(format!("Object not found: {segment_id:?}")))?;
3673 let ObjectKind::Segment { segment } = &segment_object.kind else {
3674 return Err(KclError::refactor(format!(
3675 "Object is not a segment, it is {}",
3676 segment_object.kind.human_friendly_kind_with_article()
3677 )));
3678 };
3679
3680 match segment {
3681 Segment::Point(_) => self.point_id_to_ast_reference(segment_id, new_ast),
3682 Segment::Line(_) => self.line_id_to_ast_reference(segment_id, new_ast),
3683 Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, "arc", None),
3684 Segment::Circle(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, CIRCLE_VARIABLE, None),
3685 Segment::ControlPointSpline(_) => {
3686 get_or_insert_ast_reference(new_ast, &segment_object.source, CONTROL_POINT_SPLINE_FN, None)
3687 }
3688 }
3689 }
3690
3691 fn axis_constraint_segment_to_ast(
3692 &self,
3693 segment: &ConstraintSegment,
3694 new_ast: &mut ast::Node<ast::Program>,
3695 ) -> Result<ast::Expr, KclError> {
3696 match segment {
3697 ConstraintSegment::Origin(_) => Ok(ast_name_expr("ORIGIN".to_owned())),
3698 ConstraintSegment::Segment(point_id) => self.point_id_to_ast_reference(*point_id, new_ast),
3699 }
3700 }
3701
3702 async fn add_coincident(
3703 &mut self,
3704 sketch: ObjectId,
3705 coincident: Coincident,
3706 new_ast: &mut ast::Node<ast::Program>,
3707 ) -> Result<AstNodeRef, KclError> {
3708 let sketch_id = sketch;
3709 for segment in &coincident.segments {
3710 let ConstraintSegment::Segment(segment_id) = segment else {
3711 continue;
3712 };
3713 let Some(segment_object) = self.scene_graph.objects.get(segment_id.0) else {
3714 continue;
3715 };
3716 if matches!(
3717 segment_object.kind,
3718 ObjectKind::Segment {
3719 segment: Segment::ControlPointSpline(_)
3720 }
3721 ) {
3722 return Err(KclError::refactor(
3723 "Coincident with a full controlPointSpline is not supported yet. Constrain a control point or spline edge instead."
3724 .to_owned(),
3725 ));
3726 }
3727 }
3728 let segment_asts = coincident
3729 .segments
3730 .iter()
3731 .map(|segment| self.coincident_segment_to_ast(segment, new_ast))
3732 .collect::<Result<Vec<_>, _>>()?;
3733 if segment_asts.len() < 2 {
3734 return Err(KclError::refactor(format!(
3735 "Coincident constraint must have at least 2 inputs, got {}",
3736 segment_asts.len()
3737 )));
3738 }
3739
3740 let coincident_ast = create_coincident_ast(segment_asts);
3742
3743 let (sketch_block_ref, _) = self.mutate_ast(
3745 new_ast,
3746 sketch_id,
3747 AstMutateCommand::AddSketchBlockExprStmt { expr: coincident_ast },
3748 )?;
3749 Ok(sketch_block_ref)
3750 }
3751
3752 async fn add_distance(
3753 &mut self,
3754 sketch: ObjectId,
3755 distance: Distance,
3756 new_ast: &mut ast::Node<ast::Program>,
3757 ) -> Result<AstNodeRef, KclError> {
3758 self.add_distance_constraint(sketch, DISTANCE_FN, distance, new_ast)
3759 }
3760
3761 fn distance_constraint_ast_parts(
3762 &self,
3763 function_name: &str,
3764 distance: &Distance,
3765 new_ast: &mut ast::Node<ast::Program>,
3766 ) -> Result<(ast::BinaryPart, ast::BinaryPart), KclError> {
3767 let [segment0_ast, segment1_ast] = match distance.segments.as_slice() {
3768 [pt0, pt1] => [
3769 self.coincident_segment_to_ast(pt0, new_ast)?,
3770 self.coincident_segment_to_ast(pt1, new_ast)?,
3771 ],
3772 _ => {
3773 return Err(KclError::refactor(format!(
3774 "Distance constraint must have exactly 2 segments, got {}",
3775 distance.segments.len()
3776 )));
3777 }
3778 };
3779
3780 let arguments = match &distance.label_position {
3781 Some(label_position) => vec![ast::LabeledArg {
3782 label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
3783 arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
3784 }],
3785 None => Default::default(),
3786 };
3787
3788 let call = ast::BinaryPart::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
3789 callee: ast::Node::no_src(ast_sketch2_name(function_name)),
3790 unlabeled: Some(ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(
3791 ast::ArrayExpression {
3792 elements: vec![segment0_ast, segment1_ast],
3793 digest: None,
3794 non_code_meta: Default::default(),
3795 },
3796 )))),
3797 arguments,
3798 digest: None,
3799 non_code_meta: Default::default(),
3800 })));
3801 let value = ast::BinaryPart::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
3802 value: ast::LiteralValue::Number {
3803 value: distance.distance.value,
3804 suffix: distance.distance.units,
3805 },
3806 raw: format_number_literal(distance.distance.value, distance.distance.units, None).map_err(|_| {
3807 KclError::refactor(format!(
3808 "Could not format numeric suffix: {:?}",
3809 distance.distance.units
3810 ))
3811 })?,
3812 digest: None,
3813 })));
3814
3815 Ok((call, value))
3816 }
3817
3818 fn add_distance_constraint(
3819 &mut self,
3820 sketch: ObjectId,
3821 function_name: &str,
3822 distance: Distance,
3823 new_ast: &mut ast::Node<ast::Program>,
3824 ) -> Result<AstNodeRef, KclError> {
3825 let (call, value) = self.distance_constraint_ast_parts(function_name, &distance, new_ast)?;
3826 let distance_ast = ast::Expr::BinaryExpression(BoxNode::new(ast::Node::no_src(ast::BinaryExpression {
3827 left: call,
3828 operator: ast::BinaryOperator::Eq,
3829 right: value,
3830 digest: None,
3831 })));
3832
3833 let (sketch_block_ref, _) = self.mutate_ast(
3834 new_ast,
3835 sketch,
3836 AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
3837 )?;
3838 Ok(sketch_block_ref)
3839 }
3840
3841 async fn add_angle(
3842 &mut self,
3843 sketch: ObjectId,
3844 angle: Angle,
3845 new_ast: &mut ast::Node<ast::Program>,
3846 ) -> Result<AstNodeRef, KclError> {
3847 let sketch_id = sketch;
3848 let (angle_call_ast, angle_value_ast) = self.angle_constraint_ast_parts(&angle, new_ast)?;
3849 let angle_ast = ast::Expr::BinaryExpression(BoxNode::new(ast::Node::no_src(ast::BinaryExpression {
3850 left: angle_call_ast,
3851 operator: ast::BinaryOperator::Eq,
3852 right: angle_value_ast,
3853 digest: None,
3854 })));
3855
3856 let (sketch_block_ref, _) = self.mutate_ast(
3858 new_ast,
3859 sketch_id,
3860 AstMutateCommand::AddSketchBlockExprStmt { expr: angle_ast },
3861 )?;
3862 Ok(sketch_block_ref)
3863 }
3864
3865 fn angle_constraint_ast_parts(
3866 &self,
3867 angle: &Angle,
3868 new_ast: &mut ast::Node<ast::Program>,
3869 ) -> Result<(ast::BinaryPart, ast::BinaryPart), KclError> {
3870 let &[l0_id, l1_id] = angle.lines.as_slice() else {
3871 return Err(KclError::refactor(format!(
3872 "Angle constraint must have exactly 2 lines, got {}",
3873 angle.lines.len()
3874 )));
3875 };
3876
3877 let l0_ast = self.line_id_to_ast_reference(l0_id, new_ast)?;
3878 let l1_ast = self.line_id_to_ast_reference(l1_id, new_ast)?;
3879 let lines_ast = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
3880 elements: vec![l0_ast, l1_ast],
3881 digest: None,
3882 non_code_meta: Default::default(),
3883 })));
3884
3885 if angle.inverse == Some(true) && angle.sector.is_none() {
3886 return Err(KclError::refactor("Angle inverse requires an angle sector".to_owned()));
3887 }
3888
3889 let uses_angle_dimension = angle.sector.is_some();
3890 let mut arguments = if uses_angle_dimension {
3891 vec![ast::LabeledArg {
3892 label: Some(ast::Identifier::new(ANGLE_LINES_PARAM)),
3893 arg: lines_ast.clone(),
3894 }]
3895 } else {
3896 Default::default()
3897 };
3898
3899 if let Some(sector) = angle.sector {
3900 arguments.push(ast::LabeledArg {
3901 label: Some(ast::Identifier::new(ANGLE_SECTOR_PARAM)),
3902 arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
3903 value: ast::LiteralValue::Number {
3904 value: f64::from(sector),
3905 suffix: NumericSuffix::None,
3906 },
3907 raw: sector.to_string(),
3908 digest: None,
3909 }))),
3910 });
3911 }
3912
3913 if angle.inverse == Some(true) {
3914 arguments.push(ast::LabeledArg {
3915 label: Some(ast::Identifier::new(ANGLE_INVERSE_PARAM)),
3916 arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
3917 value: ast::LiteralValue::Bool(true),
3918 raw: true.to_string(),
3919 digest: None,
3920 }))),
3921 });
3922 }
3923
3924 if let Some(label_position) = &angle.label_position {
3925 arguments.push(ast::LabeledArg {
3926 label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
3927 arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
3928 });
3929 }
3930
3931 let call = ast::BinaryPart::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
3932 callee: ast::Node::no_src(ast_sketch2_name(if uses_angle_dimension {
3933 ANGLE_DIMENSION_FN
3934 } else {
3935 ANGLE_FN
3936 })),
3937 unlabeled: (!uses_angle_dimension).then_some(lines_ast),
3938 arguments,
3939 digest: None,
3940 non_code_meta: Default::default(),
3941 })));
3942 let value = ast::BinaryPart::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
3943 value: ast::LiteralValue::Number {
3944 value: angle.angle.value,
3945 suffix: angle.angle.units,
3946 },
3947 raw: format_number_literal(angle.angle.value, angle.angle.units, None)
3948 .map_err(|_| KclError::refactor(format!("Could not format numeric suffix: {:?}", angle.angle.units)))?,
3949 digest: None,
3950 })));
3951
3952 Ok((call, value))
3953 }
3954
3955 async fn add_tangent(
3956 &mut self,
3957 sketch: ObjectId,
3958 tangent: Tangent,
3959 new_ast: &mut ast::Node<ast::Program>,
3960 ) -> Result<AstNodeRef, KclError> {
3961 let &[seg0_id, seg1_id] = tangent.input.as_slice() else {
3962 return Err(KclError::refactor(format!(
3963 "Tangent constraint must have exactly 2 segments, got {}",
3964 tangent.input.len()
3965 )));
3966 };
3967 let sketch_id = sketch;
3968
3969 let seg0_object = self
3970 .scene_graph
3971 .objects
3972 .get(seg0_id.0)
3973 .ok_or_else(|| KclError::refactor(format!("Segment not found: {seg0_id:?}")))?;
3974 let ObjectKind::Segment { segment: seg0_segment } = &seg0_object.kind else {
3975 return Err(KclError::refactor(format!("Object is not a segment: {seg0_object:?}")));
3976 };
3977 let seg0_ast = match seg0_segment {
3978 Segment::Line(_) | Segment::Arc(_) | Segment::Circle(_) => {
3979 self.segment_id_to_constraint_ast_reference(seg0_id, new_ast)?
3980 }
3981 _ => {
3982 return Err(KclError::refactor(format!(
3983 "Tangent supports only line/arc/circle segments for now, got: {seg0_segment:?}"
3984 )));
3985 }
3986 };
3987
3988 let seg1_object = self
3989 .scene_graph
3990 .objects
3991 .get(seg1_id.0)
3992 .ok_or_else(|| KclError::refactor(format!("Segment not found: {seg1_id:?}")))?;
3993 let ObjectKind::Segment { segment: seg1_segment } = &seg1_object.kind else {
3994 return Err(KclError::refactor(format!("Object is not a segment: {seg1_object:?}")));
3995 };
3996 let seg1_ast = match seg1_segment {
3997 Segment::Line(_) | Segment::Arc(_) | Segment::Circle(_) => {
3998 self.segment_id_to_constraint_ast_reference(seg1_id, new_ast)?
3999 }
4000 _ => {
4001 return Err(KclError::refactor(format!(
4002 "Tangent supports only line/arc/circle segments for now, got: {seg1_segment:?}"
4003 )));
4004 }
4005 };
4006
4007 let tangent_ast = create_tangent_ast(seg0_ast, seg1_ast);
4008 let (sketch_block_ref, _) = self.mutate_ast(
4009 new_ast,
4010 sketch_id,
4011 AstMutateCommand::AddSketchBlockExprStmt { expr: tangent_ast },
4012 )?;
4013 Ok(sketch_block_ref)
4014 }
4015
4016 async fn add_symmetric(
4017 &mut self,
4018 sketch: ObjectId,
4019 symmetric: Symmetric,
4020 new_ast: &mut ast::Node<ast::Program>,
4021 ) -> Result<AstNodeRef, KclError> {
4022 let &[input0_id, input1_id] = symmetric.input.as_slice() else {
4023 return Err(KclError::refactor(format!(
4024 "Symmetric constraint must have exactly 2 inputs, got {}",
4025 symmetric.input.len()
4026 )));
4027 };
4028 let sketch_id = sketch;
4029
4030 let input0_ast = self.symmetric_input_id_to_ast_reference(input0_id, new_ast)?;
4031 let input1_ast = self.symmetric_input_id_to_ast_reference(input1_id, new_ast)?;
4032 let axis_ast = self.symmetric_axis_id_to_ast_reference(symmetric.axis, new_ast)?;
4033
4034 let symmetric_ast = create_symmetric_ast(vec![input0_ast, input1_ast], axis_ast);
4035 let (sketch_block_ref, _) = self.mutate_ast(
4036 new_ast,
4037 sketch_id,
4038 AstMutateCommand::AddSketchBlockExprStmt { expr: symmetric_ast },
4039 )?;
4040 Ok(sketch_block_ref)
4041 }
4042
4043 async fn add_midpoint(
4044 &mut self,
4045 sketch: ObjectId,
4046 midpoint: Midpoint,
4047 new_ast: &mut ast::Node<ast::Program>,
4048 ) -> Result<AstNodeRef, KclError> {
4049 let sketch_id = sketch;
4050 let point_ast = self.axis_constraint_segment_to_ast(&midpoint.point, new_ast)?;
4051
4052 let segment_object = self
4053 .scene_graph
4054 .objects
4055 .get(midpoint.segment.0)
4056 .ok_or_else(|| KclError::refactor(format!("Segment not found: {:?}", midpoint.segment)))?;
4057 let ObjectKind::Segment {
4058 segment: midpoint_segment,
4059 } = &segment_object.kind
4060 else {
4061 return Err(KclError::refactor(format!(
4062 "Object must be a segment, but it was {}",
4063 segment_object.kind.human_friendly_kind_with_article()
4064 )));
4065 };
4066 let segment_ast = match midpoint_segment {
4067 Segment::Line(_) => self.line_id_to_ast_reference(midpoint.segment, new_ast)?,
4068 Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, "arc", None)?,
4069 _ => {
4070 return Err(KclError::refactor(format!(
4071 "Midpoint target must be a line or arc segment but it was {}",
4072 midpoint_segment.human_friendly_kind_with_article()
4073 )));
4074 }
4075 };
4076
4077 let midpoint_ast = create_midpoint_ast(segment_ast, point_ast);
4078 let (sketch_block_ref, _) = self.mutate_ast(
4079 new_ast,
4080 sketch_id,
4081 AstMutateCommand::AddSketchBlockExprStmt { expr: midpoint_ast },
4082 )?;
4083 Ok(sketch_block_ref)
4084 }
4085
4086 async fn add_equal_radius(
4087 &mut self,
4088 sketch: ObjectId,
4089 equal_radius: EqualRadius,
4090 new_ast: &mut ast::Node<ast::Program>,
4091 ) -> Result<AstNodeRef, KclError> {
4092 if equal_radius.input.len() < 2 {
4093 return Err(KclError::refactor(format!(
4094 "equalRadius constraint must have at least 2 segments, got {}",
4095 equal_radius.input.len()
4096 )));
4097 }
4098
4099 let sketch_id = sketch;
4100 let input_asts = equal_radius
4101 .input
4102 .iter()
4103 .map(|segment_id| self.equal_radius_segment_id_to_ast_reference(*segment_id, new_ast))
4104 .collect::<Result<Vec<_>, _>>()?;
4105
4106 let equal_radius_ast = create_equal_radius_ast(input_asts);
4107 let (sketch_block_ref, _) = self.mutate_ast(
4108 new_ast,
4109 sketch_id,
4110 AstMutateCommand::AddSketchBlockExprStmt { expr: equal_radius_ast },
4111 )?;
4112 Ok(sketch_block_ref)
4113 }
4114
4115 async fn add_radius(
4116 &mut self,
4117 sketch: ObjectId,
4118 radius: Radius,
4119 new_ast: &mut ast::Node<ast::Program>,
4120 ) -> Result<AstNodeRef, KclError> {
4121 let params = ArcSizeConstraintParams {
4122 points: vec![radius.arc],
4123 function_name: RADIUS_FN,
4124 value: radius.radius.value,
4125 units: radius.radius.units,
4126 label_position: radius.label_position,
4127 constraint_type_name: "Radius",
4128 };
4129 self.add_arc_size_constraint(sketch, params, new_ast).await
4130 }
4131
4132 async fn add_diameter(
4133 &mut self,
4134 sketch: ObjectId,
4135 diameter: Diameter,
4136 new_ast: &mut ast::Node<ast::Program>,
4137 ) -> Result<AstNodeRef, KclError> {
4138 let params = ArcSizeConstraintParams {
4139 points: vec![diameter.arc],
4140 function_name: DIAMETER_FN,
4141 value: diameter.diameter.value,
4142 units: diameter.diameter.units,
4143 label_position: diameter.label_position,
4144 constraint_type_name: "Diameter",
4145 };
4146 self.add_arc_size_constraint(sketch, params, new_ast).await
4147 }
4148
4149 async fn add_fixed_constraints(
4150 &mut self,
4151 sketch: ObjectId,
4152 points: Vec<FixedPoint>,
4153 new_ast: &mut ast::Node<ast::Program>,
4154 ) -> Result<AstNodeRef, KclError> {
4155 let mut sketch_block_ref = None;
4156
4157 for fixed_point in points {
4158 let point_ast = self.point_id_to_ast_reference(fixed_point.point, new_ast)?;
4159 let fixed_ast = create_fixed_point_constraint_ast(point_ast, fixed_point.position)
4160 .map_err(|err| KclError::refactor(err.to_string()))?;
4161
4162 let (sketch_ref, _) = self.mutate_ast(
4163 new_ast,
4164 sketch,
4165 AstMutateCommand::AddSketchBlockExprStmt { expr: fixed_ast },
4166 )?;
4167 sketch_block_ref = Some(sketch_ref);
4168 }
4169
4170 sketch_block_ref.ok_or_else(|| KclError::refactor("Fixed constraint requires at least one point".to_owned()))
4171 }
4172
4173 async fn add_arc_size_constraint(
4174 &mut self,
4175 sketch: ObjectId,
4176 params: ArcSizeConstraintParams,
4177 new_ast: &mut ast::Node<ast::Program>,
4178 ) -> Result<AstNodeRef, KclError> {
4179 let sketch_id = sketch;
4180
4181 if params.points.len() != 1 {
4183 return Err(KclError::refactor(format!(
4184 "{} constraint must have exactly 1 argument (an arc segment), got {}",
4185 params.constraint_type_name,
4186 params.points.len()
4187 )));
4188 }
4189
4190 let arc_id = params.points[0];
4191 let arc_object = self
4192 .scene_graph
4193 .objects
4194 .get(arc_id.0)
4195 .ok_or_else(|| KclError::refactor(format!("Arc segment not found: {arc_id:?}")))?;
4196 let ObjectKind::Segment { segment: arc_segment } = &arc_object.kind else {
4197 return Err(KclError::refactor(format!("Object is not a segment: {arc_object:?}")));
4198 };
4199 let ref_type = match arc_segment {
4200 Segment::Arc(_) => ARC_VARIABLE,
4201 Segment::Circle(_) => CIRCLE_VARIABLE,
4202 _ => {
4203 return Err(KclError::refactor(format!(
4204 "{} constraint argument must be an arc or circle segment, got: {arc_segment:?}",
4205 params.constraint_type_name
4206 )));
4207 }
4208 };
4209 let arc_ast = get_or_insert_ast_reference(new_ast, &arc_object.source, ref_type, None)?;
4211 let arguments = match ¶ms.label_position {
4212 Some(label_position) => vec![ast::LabeledArg {
4213 label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
4214 arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
4215 }],
4216 None => Default::default(),
4217 };
4218
4219 let call_ast = ast::BinaryPart::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
4221 callee: ast::Node::no_src(ast_sketch2_name(params.function_name)),
4222 unlabeled: Some(arc_ast),
4223 arguments,
4224 digest: None,
4225 non_code_meta: Default::default(),
4226 })));
4227 let constraint_ast = ast::Expr::BinaryExpression(BoxNode::new(ast::Node::no_src(ast::BinaryExpression {
4228 left: call_ast,
4229 operator: ast::BinaryOperator::Eq,
4230 right: ast::BinaryPart::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
4231 value: ast::LiteralValue::Number {
4232 value: params.value,
4233 suffix: params.units,
4234 },
4235 raw: format_number_literal(params.value, params.units, None)
4236 .map_err(|_| KclError::refactor(format!("Could not format numeric suffix: {:?}", params.units)))?,
4237 digest: None,
4238 }))),
4239 digest: None,
4240 })));
4241
4242 let (sketch_block_ref, _) = self.mutate_ast(
4244 new_ast,
4245 sketch_id,
4246 AstMutateCommand::AddSketchBlockExprStmt { expr: constraint_ast },
4247 )?;
4248 Ok(sketch_block_ref)
4249 }
4250
4251 async fn add_horizontal_distance(
4252 &mut self,
4253 sketch: ObjectId,
4254 distance: Distance,
4255 new_ast: &mut ast::Node<ast::Program>,
4256 ) -> Result<AstNodeRef, KclError> {
4257 self.add_distance_constraint(sketch, HORIZONTAL_DISTANCE_FN, distance, new_ast)
4258 }
4259
4260 async fn add_vertical_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, VERTICAL_DISTANCE_FN, distance, new_ast)
4267 }
4268
4269 async fn add_horizontal(
4270 &mut self,
4271 sketch: ObjectId,
4272 horizontal: Horizontal,
4273 new_ast: &mut ast::Node<ast::Program>,
4274 ) -> Result<AstNodeRef, KclError> {
4275 let sketch_id = sketch;
4276
4277 let first_arg_ast = match horizontal {
4279 Horizontal::Line { line } => {
4280 let line_object = self
4281 .scene_graph
4282 .objects
4283 .get(line.0)
4284 .ok_or_else(|| KclError::refactor(format!("Line not found: {line:?}")))?;
4285 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4286 let kind = line_object.kind.human_friendly_kind_with_article();
4287 return Err(KclError::refactor(format!(
4288 "This constraint only works on Segments, but you selected {kind}"
4289 )));
4290 };
4291 let Segment::Line(_) = line_segment else {
4292 return Err(KclError::refactor(format!(
4293 "Only lines can be made horizontal, but you selected {}",
4294 line_segment.human_friendly_kind_with_article(),
4295 )));
4296 };
4297 self.line_id_to_ast_reference(line, new_ast)?
4298 }
4299 Horizontal::Points { points } => {
4300 let point_asts = points
4301 .iter()
4302 .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
4303 .collect::<Result<Vec<_>, _>>()?;
4304 ast::ArrayExpression::new(point_asts).into()
4305 }
4306 };
4307 let horizontal_ast = create_horizontal_ast(first_arg_ast);
4309
4310 let (sketch_block_ref, _) = self.mutate_ast(
4312 new_ast,
4313 sketch_id,
4314 AstMutateCommand::AddSketchBlockExprStmt { expr: horizontal_ast },
4315 )?;
4316 Ok(sketch_block_ref)
4317 }
4318
4319 async fn add_lines_equal_length(
4320 &mut self,
4321 sketch: ObjectId,
4322 lines_equal_length: LinesEqualLength,
4323 new_ast: &mut ast::Node<ast::Program>,
4324 ) -> Result<AstNodeRef, KclError> {
4325 if lines_equal_length.lines.len() < 2 {
4326 return Err(KclError::refactor(format!(
4327 "Lines equal length constraint must have at least 2 lines, got {}",
4328 lines_equal_length.lines.len()
4329 )));
4330 };
4331
4332 let sketch_id = sketch;
4333
4334 let line_asts = lines_equal_length
4336 .lines
4337 .iter()
4338 .map(|line_id| {
4339 let line_object = self
4340 .scene_graph
4341 .objects
4342 .get(line_id.0)
4343 .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
4344 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4345 let kind = line_object.kind.human_friendly_kind_with_article();
4346 return Err(KclError::refactor(format!(
4347 "This constraint only works on Segments, but you selected {kind}"
4348 )));
4349 };
4350 let Segment::Line(_) = line_segment else {
4351 let kind = line_segment.human_friendly_kind_with_article();
4352 return Err(KclError::refactor(format!(
4353 "Only lines can be made equal length, but you selected {kind}"
4354 )));
4355 };
4356
4357 self.line_id_to_ast_reference(*line_id, new_ast)
4358 })
4359 .collect::<Result<Vec<_>, _>>()?;
4360
4361 let equal_length_ast = create_equal_length_ast(line_asts);
4363
4364 let (sketch_block_ref, _) = self.mutate_ast(
4366 new_ast,
4367 sketch_id,
4368 AstMutateCommand::AddSketchBlockExprStmt { expr: equal_length_ast },
4369 )?;
4370 Ok(sketch_block_ref)
4371 }
4372
4373 fn equal_radius_segment_id_to_ast_reference(
4374 &mut self,
4375 segment_id: ObjectId,
4376 new_ast: &mut ast::Node<ast::Program>,
4377 ) -> Result<ast::Expr, KclError> {
4378 let segment_object = self
4379 .scene_graph
4380 .objects
4381 .get(segment_id.0)
4382 .ok_or_else(|| KclError::refactor(format!("Segment not found: {segment_id:?}")))?;
4383 let ObjectKind::Segment { segment } = &segment_object.kind else {
4384 return Err(KclError::refactor(format!(
4385 "Object is not a segment, it was {}",
4386 segment_object.kind.human_friendly_kind_with_article()
4387 )));
4388 };
4389
4390 let ref_type = match segment {
4391 Segment::Arc(_) => ARC_VARIABLE,
4392 Segment::Circle(_) => CIRCLE_VARIABLE,
4393 _ => {
4394 return Err(KclError::refactor(format!(
4395 "equalRadius supports only arc/circle segments, got {}",
4396 segment.human_friendly_kind_with_article()
4397 )));
4398 }
4399 };
4400
4401 get_or_insert_ast_reference(new_ast, &segment_object.source, ref_type, None)
4402 }
4403
4404 fn symmetric_input_id_to_ast_reference(
4405 &mut self,
4406 segment_id: ObjectId,
4407 new_ast: &mut ast::Node<ast::Program>,
4408 ) -> Result<ast::Expr, KclError> {
4409 let segment_object = self
4410 .scene_graph
4411 .objects
4412 .get(segment_id.0)
4413 .ok_or_else(|| KclError::refactor(format!("Segment not found: {segment_id:?}")))?;
4414 let ObjectKind::Segment { segment } = &segment_object.kind else {
4415 return Err(KclError::refactor(format!(
4416 "Object is not a segment, it was {}",
4417 segment_object.kind.human_friendly_kind_with_article()
4418 )));
4419 };
4420
4421 match segment {
4422 Segment::Point(_) => self.point_id_to_ast_reference(segment_id, new_ast),
4423 Segment::Line(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, LINE_VARIABLE, None),
4424 Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, ARC_VARIABLE, None),
4425 Segment::Circle(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, CIRCLE_VARIABLE, None),
4426 Segment::ControlPointSpline(_) => Err(KclError::refactor(
4427 "Symmetric does not yet support control point splines".to_owned(),
4428 )),
4429 }
4430 }
4431
4432 fn symmetric_axis_id_to_ast_reference(
4433 &mut self,
4434 segment_id: ObjectId,
4435 new_ast: &mut ast::Node<ast::Program>,
4436 ) -> Result<ast::Expr, KclError> {
4437 let segment_object = self
4438 .scene_graph
4439 .objects
4440 .get(segment_id.0)
4441 .ok_or_else(|| KclError::refactor(format!("Axis segment not found: {segment_id:?}")))?;
4442 let ObjectKind::Segment { segment } = &segment_object.kind else {
4443 return Err(KclError::refactor(format!(
4444 "Object is not a segment, it was {}",
4445 segment_object.kind.human_friendly_kind_with_article()
4446 )));
4447 };
4448 match segment {
4449 Segment::Line(_) => self.line_id_to_ast_reference(segment_id, new_ast),
4450 _ => Err(KclError::refactor(format!(
4451 "Symmetric axis must be a line, got {}",
4452 segment.human_friendly_kind_with_article()
4453 ))),
4454 }
4455 }
4456
4457 async fn add_parallel(
4458 &mut self,
4459 sketch: ObjectId,
4460 parallel: Parallel,
4461 new_ast: &mut ast::Node<ast::Program>,
4462 ) -> Result<AstNodeRef, KclError> {
4463 if parallel.lines.len() < 2 {
4464 return Err(KclError::refactor(format!(
4465 "Parallel constraint must have at least 2 lines, got {}",
4466 parallel.lines.len()
4467 )));
4468 };
4469
4470 let sketch_id = sketch;
4471
4472 let line_asts = parallel
4473 .lines
4474 .iter()
4475 .map(|line_id| {
4476 let line_object = self
4477 .scene_graph
4478 .objects
4479 .get(line_id.0)
4480 .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
4481 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4482 let kind = line_object.kind.human_friendly_kind_with_article();
4483 return Err(KclError::refactor(format!(
4484 "This constraint only works on Segments, but you selected {kind}"
4485 )));
4486 };
4487 let Segment::Line(_) = line_segment else {
4488 let kind = line_segment.human_friendly_kind_with_article();
4489 return Err(KclError::refactor(format!(
4490 "Only lines can be made parallel, but you selected {kind}"
4491 )));
4492 };
4493
4494 self.line_id_to_ast_reference(*line_id, new_ast)
4495 })
4496 .collect::<Result<Vec<_>, _>>()?;
4497
4498 let call_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
4499 callee: ast::Node::no_src(ast_sketch2_name(LinesAtAngleKind::Parallel.to_function_name())),
4500 unlabeled: Some(ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(
4501 ast::ArrayExpression {
4502 elements: line_asts,
4503 digest: None,
4504 non_code_meta: Default::default(),
4505 },
4506 )))),
4507 arguments: Default::default(),
4508 digest: None,
4509 non_code_meta: Default::default(),
4510 })));
4511
4512 let (sketch_block_ref, _) = self.mutate_ast(
4513 new_ast,
4514 sketch_id,
4515 AstMutateCommand::AddSketchBlockExprStmt { expr: call_ast },
4516 )?;
4517 Ok(sketch_block_ref)
4518 }
4519
4520 async fn add_perpendicular(
4521 &mut self,
4522 sketch: ObjectId,
4523 perpendicular: Perpendicular,
4524 new_ast: &mut ast::Node<ast::Program>,
4525 ) -> Result<AstNodeRef, KclError> {
4526 self.add_lines_at_angle_constraint(sketch, LinesAtAngleKind::Perpendicular, perpendicular.lines, new_ast)
4527 .await
4528 }
4529
4530 async fn add_lines_at_angle_constraint(
4531 &mut self,
4532 sketch: ObjectId,
4533 angle_kind: LinesAtAngleKind,
4534 lines: Vec<ObjectId>,
4535 new_ast: &mut ast::Node<ast::Program>,
4536 ) -> Result<AstNodeRef, KclError> {
4537 let &[line0_id, line1_id] = lines.as_slice() else {
4538 return Err(KclError::refactor(format!(
4539 "{} constraint must have exactly 2 lines, got {}",
4540 angle_kind.to_function_name(),
4541 lines.len()
4542 )));
4543 };
4544
4545 let sketch_id = sketch;
4546
4547 let line0_object = self
4549 .scene_graph
4550 .objects
4551 .get(line0_id.0)
4552 .ok_or_else(|| KclError::refactor(format!("Line not found: {line0_id:?}")))?;
4553 let ObjectKind::Segment { segment: line0_segment } = &line0_object.kind else {
4554 let kind = line0_object.kind.human_friendly_kind_with_article();
4555 return Err(KclError::refactor(format!(
4556 "This constraint only works on Segments, but you selected {kind}"
4557 )));
4558 };
4559 let Segment::Line(_) = line0_segment else {
4560 return Err(KclError::refactor(format!(
4561 "Only lines can be made {}, but you selected {}",
4562 angle_kind.to_function_name(),
4563 line0_segment.human_friendly_kind_with_article(),
4564 )));
4565 };
4566 let line0_ast = self.line_id_to_ast_reference(line0_id, new_ast)?;
4567
4568 let line1_object = self
4569 .scene_graph
4570 .objects
4571 .get(line1_id.0)
4572 .ok_or_else(|| KclError::refactor(format!("Line not found: {line1_id:?}")))?;
4573 let ObjectKind::Segment { segment: line1_segment } = &line1_object.kind else {
4574 let kind = line1_object.kind.human_friendly_kind_with_article();
4575 return Err(KclError::refactor(format!(
4576 "This constraint only works on Segments, but you selected {kind}"
4577 )));
4578 };
4579 let Segment::Line(_) = line1_segment else {
4580 return Err(KclError::refactor(format!(
4581 "Only lines can be made {}, but you selected {}",
4582 angle_kind.to_function_name(),
4583 line1_segment.human_friendly_kind_with_article(),
4584 )));
4585 };
4586 let line1_ast = self.line_id_to_ast_reference(line1_id, new_ast)?;
4587
4588 let call_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
4590 callee: ast::Node::no_src(ast_sketch2_name(angle_kind.to_function_name())),
4591 unlabeled: Some(ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(
4592 ast::ArrayExpression {
4593 elements: vec![line0_ast, line1_ast],
4594 digest: None,
4595 non_code_meta: Default::default(),
4596 },
4597 )))),
4598 arguments: Default::default(),
4599 digest: None,
4600 non_code_meta: Default::default(),
4601 })));
4602
4603 let (sketch_block_ref, _) = self.mutate_ast(
4605 new_ast,
4606 sketch_id,
4607 AstMutateCommand::AddSketchBlockExprStmt { expr: call_ast },
4608 )?;
4609 Ok(sketch_block_ref)
4610 }
4611
4612 async fn add_vertical(
4613 &mut self,
4614 sketch: ObjectId,
4615 vertical: Vertical,
4616 new_ast: &mut ast::Node<ast::Program>,
4617 ) -> Result<AstNodeRef, KclError> {
4618 let sketch_id = sketch;
4619
4620 let first_arg_ast = match vertical {
4621 Vertical::Line { line } => {
4622 let line_object = self
4624 .scene_graph
4625 .objects
4626 .get(line.0)
4627 .ok_or_else(|| KclError::refactor(format!("Line not found: {line:?}")))?;
4628 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4629 let kind = line_object.kind.human_friendly_kind_with_article();
4630 return Err(KclError::refactor(format!(
4631 "This constraint only works on Segments, but you selected {kind}"
4632 )));
4633 };
4634 let Segment::Line(_) = line_segment else {
4635 return Err(KclError::refactor(format!(
4636 "Only lines can be made vertical, but you selected {}",
4637 line_segment.human_friendly_kind_with_article()
4638 )));
4639 };
4640 self.line_id_to_ast_reference(line, new_ast)?
4641 }
4642 Vertical::Points { points } => {
4643 let point_asts = points
4644 .iter()
4645 .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
4646 .collect::<Result<Vec<_>, _>>()?;
4647 ast::ArrayExpression::new(point_asts).into()
4648 }
4649 };
4650 let vertical_ast = create_vertical_ast(first_arg_ast);
4652
4653 let (sketch_block_ref, _) = self.mutate_ast(
4655 new_ast,
4656 sketch_id,
4657 AstMutateCommand::AddSketchBlockExprStmt { expr: vertical_ast },
4658 )?;
4659 Ok(sketch_block_ref)
4660 }
4661
4662 async fn execute_after_add_constraint(
4663 &mut self,
4664 ctx: &ExecutorContext,
4665 sketch_id: ObjectId,
4666 sketch_block_ref: AstNodeRef,
4667 new_ast: &mut ast::Node<ast::Program>,
4668 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
4669 let new_source = source_from_ast(new_ast);
4671 let new_program = parse_frontend_mutation_source(
4673 &new_source,
4674 "Error parsing KCL source after adding constraint",
4675 "No AST produced after adding constraint",
4676 )?;
4677 let constraint_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
4678 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
4679 "Source range of new constraint not found in sketch block: {sketch_block_ref:?}; {err:?}"
4680 )))
4681 })?;
4682
4683 let mut truncated_program = new_program.clone();
4686 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
4687 .map_err(KclErrorWithOutputs::no_outputs)?;
4688
4689 let outcome = ctx
4691 .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch_id))
4692 .await?;
4693
4694 let new_object_ids = {
4695 let constraint_id = outcome
4697 .source_range_to_object
4698 .get(&constraint_node_ref.range)
4699 .copied()
4700 .ok_or_else(|| {
4701 KclErrorWithOutputs::from_error_outcome(
4702 KclError::refactor(format!("Source range of constraint not found: {constraint_node_ref:?}")),
4703 outcome.clone(),
4704 )
4705 })?;
4706 vec![constraint_id]
4707 };
4708
4709 self.program = new_program;
4712
4713 let outcome = self.update_state_after_exec(outcome, true);
4715
4716 let src_delta = self.commit_var_solutions_to_program(&outcome, "adding constraint")?;
4717 let scene_graph_delta = SceneGraphDelta {
4718 new_graph: self.scene_graph_for_ui(),
4719 invalidates_ids: false,
4720 new_objects: new_object_ids,
4721 exec_outcome: outcome,
4722 };
4723 Ok((src_delta, scene_graph_delta))
4724 }
4725
4726 fn commit_var_solutions_to_program(&mut self, outcome: &ExecOutcome, operation: &str) -> ExecResult<SourceDelta> {
4727 let commit_failure = || {
4728 KclErrorWithOutputs::from_error_outcome(
4729 KclError::refactor(format!("Could not update KCL after {operation}.")),
4730 outcome.clone(),
4731 )
4732 };
4733
4734 let default_length_unit = self.default_length_unit();
4735 let mut settled_ast = self.program.ast.clone();
4736 let mut committed_solver_value = false;
4737 for (var_range, node_path, value) in &outcome.var_solutions {
4738 let Some(lookup) = numeric_literal_at_node_path(&settled_ast, node_path.as_ref(), *var_range) else {
4739 return Err(commit_failure());
4740 };
4741 let new_value = match &lookup {
4742 Some(current_literal) => {
4743 if !var_solution_needs_commit(current_literal, *value, default_length_unit) {
4744 continue;
4745 }
4746 preserve_var_solution_literal_style(current_literal, *value, default_length_unit)
4747 }
4748 None => {
4749 Number {
4753 value: number_value_in_default_length_units(*value, default_length_unit),
4754 units: default_length_unit.into(),
4755 }
4756 }
4757 };
4758 committed_solver_value = true;
4759 let source_ref = SourceRef::Simple {
4760 range: *var_range,
4761 node_path: node_path.clone(),
4762 };
4763 mutate_ast_node_by_source_ref(
4764 &mut settled_ast,
4765 &source_ref,
4766 AstMutateCommand::EditVarInitialValue { value: new_value },
4767 )
4768 .map_err(|_| commit_failure())?;
4769 }
4770
4771 if !committed_solver_value {
4772 return Ok(SourceDelta {
4773 text: self.program.original_file_contents.clone(),
4774 });
4775 }
4776
4777 let settled_source = source_from_ast(&settled_ast);
4778 let (settled_program, errors) = Program::parse(&settled_source).map_err(|_| commit_failure())?;
4779 if !errors.is_empty() {
4780 return Err(commit_failure());
4781 }
4782 let Some(settled_program) = settled_program else {
4783 return Err(commit_failure());
4784 };
4785
4786 self.program = settled_program;
4787
4788 Ok(SourceDelta { text: settled_source })
4789 }
4790
4791 fn segment_will_be_deleted(&self, segment_id: ObjectId, segment_ids_set: &AhashIndexSet<ObjectId>) -> bool {
4793 if segment_ids_set.contains(&segment_id) {
4794 return true;
4795 }
4796
4797 let Some(segment_object) = self.scene_graph.objects.get(segment_id.0) else {
4798 return false;
4799 };
4800 let ObjectKind::Segment { segment } = &segment_object.kind else {
4801 return false;
4802 };
4803 let Segment::Point(point) = segment else {
4804 return false;
4805 };
4806
4807 point.owner.is_some_and(|owner_id| segment_ids_set.contains(&owner_id))
4808 }
4809
4810 fn remaining_constraint_segments(
4811 &self,
4812 segments: &[ConstraintSegment],
4813 segment_ids_set: &AhashIndexSet<ObjectId>,
4814 ) -> Vec<ConstraintSegment> {
4815 segments
4816 .iter()
4817 .copied()
4818 .filter(|segment| match segment {
4819 ConstraintSegment::Origin(_) => true,
4820 ConstraintSegment::Segment(segment_id) => !self.segment_will_be_deleted(*segment_id, segment_ids_set),
4821 })
4822 .collect()
4823 }
4824
4825 fn find_referenced_constraints(
4826 &self,
4827 sketch_id: ObjectId,
4828 segment_ids_set: &AhashIndexSet<ObjectId>,
4829 ) -> Result<AhashIndexSet<ObjectId>, KclError> {
4830 let sketch_object = self
4832 .scene_graph
4833 .objects
4834 .get(sketch_id.0)
4835 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch_id:?}")))?;
4836 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
4837 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
4838 };
4839 let segment_or_owner_matches = |segment_id: ObjectId| {
4840 if segment_ids_set.contains(&segment_id) {
4841 return true;
4842 }
4843 let segment_object = self.scene_graph.objects.get(segment_id.0);
4844 if let Some(obj) = segment_object
4845 && let ObjectKind::Segment { segment } = &obj.kind
4846 {
4847 match segment {
4848 Segment::Point(point) => point.owner.is_some_and(|owner_id| segment_ids_set.contains(&owner_id)),
4849 Segment::Line(line) => line.owner.is_some_and(|owner_id| segment_ids_set.contains(&owner_id)),
4850 _ => false,
4851 }
4852 } else {
4853 false
4854 }
4855 };
4856 let mut constraint_ids_set = AhashIndexSet::default();
4857 for constraint_id in &sketch.constraints {
4858 let constraint_object = self
4859 .scene_graph
4860 .objects
4861 .get(constraint_id.0)
4862 .ok_or_else(|| KclError::refactor(format!("Constraint not found: {constraint_id:?}")))?;
4863 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
4864 return Err(KclError::refactor(format!(
4865 "Object is not a constraint, it is {}",
4866 constraint_object.kind.human_friendly_kind_with_article()
4867 )));
4868 };
4869 let depends_on_segment = match constraint {
4870 Constraint::Coincident(c) => c.segment_ids().any(segment_or_owner_matches),
4871 Constraint::Distance(d) => d.segment_ids().any(segment_or_owner_matches),
4872 Constraint::Fixed(fixed) => fixed
4873 .points
4874 .iter()
4875 .any(|fixed_point| self.segment_will_be_deleted(fixed_point.point, segment_ids_set)),
4876 Constraint::Radius(r) => segment_or_owner_matches(r.arc),
4877 Constraint::Diameter(d) => segment_or_owner_matches(d.arc),
4878 Constraint::EqualRadius(equal_radius) => {
4879 equal_radius.input.iter().copied().any(segment_or_owner_matches)
4880 }
4881 Constraint::HorizontalDistance(d) => d.segment_ids().any(segment_or_owner_matches),
4882 Constraint::VerticalDistance(d) => d.segment_ids().any(segment_or_owner_matches),
4883 Constraint::Horizontal(h) => match h {
4884 Horizontal::Line { line } => segment_or_owner_matches(*line),
4885 Horizontal::Points { points } => points.iter().any(|point| match point {
4886 ConstraintSegment::Segment(point) => segment_or_owner_matches(*point),
4887 ConstraintSegment::Origin(_) => false,
4888 }),
4889 },
4890 Constraint::Vertical(v) => match v {
4891 Vertical::Line { line } => segment_or_owner_matches(*line),
4892 Vertical::Points { points } => points.iter().any(|point| match point {
4893 ConstraintSegment::Segment(point) => segment_or_owner_matches(*point),
4894 ConstraintSegment::Origin(_) => false,
4895 }),
4896 },
4897 Constraint::LinesEqualLength(lines_equal_length) => {
4898 lines_equal_length.lines.iter().copied().any(segment_or_owner_matches)
4899 }
4900 Constraint::Midpoint(midpoint) => {
4901 segment_or_owner_matches(midpoint.segment)
4902 || matches!(
4903 midpoint.point,
4904 ConstraintSegment::Segment(point) if segment_or_owner_matches(point)
4905 )
4906 }
4907 Constraint::Parallel(parallel) => parallel.lines.iter().copied().any(segment_or_owner_matches),
4908 Constraint::Perpendicular(perpendicular) => {
4909 perpendicular.lines.iter().copied().any(segment_or_owner_matches)
4910 }
4911 Constraint::Angle(angle) => angle.lines.iter().copied().any(segment_or_owner_matches),
4912 Constraint::Symmetric(symmetric) => {
4913 segment_or_owner_matches(symmetric.axis)
4914 || symmetric.input.iter().copied().any(segment_or_owner_matches)
4915 }
4916 Constraint::Tangent(tangent) => tangent.input.iter().copied().any(segment_or_owner_matches),
4917 };
4918 if depends_on_segment {
4919 constraint_ids_set.insert(*constraint_id);
4920 }
4921 }
4922 Ok(constraint_ids_set)
4923 }
4924
4925 fn update_state_after_exec(&mut self, outcome: ExecOutcome, freedom_analysis_ran: bool) -> ExecOutcome {
4926 let mut outcome = outcome;
4927 self.solid_references = solid_references_from_variables(&self.program.ast, &outcome.variables);
4928 let mut new_objects = std::mem::take(&mut outcome.scene_objects);
4929
4930 if freedom_analysis_ran {
4931 self.point_freedom_cache.clear();
4934 for new_obj in &new_objects {
4935 if let ObjectKind::Segment {
4936 segment: kcl_api::Segment::Point(point),
4937 } = &new_obj.kind
4938 {
4939 self.point_freedom_cache.insert(new_obj.id, point.freedom);
4940 }
4941 }
4942 add_wall_and_cap_face_objects(&mut new_objects, &outcome.artifact_graph);
4943 self.scene_graph.objects = new_objects;
4945 } else {
4946 for old_obj in &self.scene_graph.objects {
4949 if let ObjectKind::Segment {
4950 segment: kcl_api::Segment::Point(point),
4951 } = &old_obj.kind
4952 {
4953 self.point_freedom_cache.insert(old_obj.id, point.freedom);
4954 }
4955 }
4956
4957 let mut updated_objects = Vec::with_capacity(new_objects.len());
4959 for new_obj in new_objects {
4960 let mut obj = new_obj;
4961 if let ObjectKind::Segment {
4962 segment: kcl_api::Segment::Point(point),
4963 } = &mut obj.kind
4964 {
4965 let new_freedom = point.freedom;
4966 match new_freedom {
4972 Freedom::Free => {
4973 match self.point_freedom_cache.get(&obj.id).copied() {
4974 Some(Freedom::Conflict) => {
4975 }
4978 Some(Freedom::Fixed) => {
4979 point.freedom = Freedom::Fixed;
4981 }
4982 Some(Freedom::Free) => {
4983 }
4985 None => {
4986 }
4988 }
4989 }
4990 Freedom::Fixed => {
4991 }
4993 Freedom::Conflict => {
4994 }
4996 }
4997 self.point_freedom_cache.insert(obj.id, point.freedom);
4999 }
5000 updated_objects.push(obj);
5001 }
5002
5003 add_wall_and_cap_face_objects(&mut updated_objects, &outcome.artifact_graph);
5004 self.scene_graph.objects = updated_objects;
5005 }
5006 outcome
5007 }
5008
5009 fn mutate_ast(
5010 &mut self,
5011 ast: &mut ast::Node<ast::Program>,
5012 object_id: ObjectId,
5013 command: AstMutateCommand,
5014 ) -> Result<(AstNodeRef, AstMutateCommandReturn), KclError> {
5015 let sketch_object = self
5016 .scene_graph
5017 .objects
5018 .get(object_id.0)
5019 .ok_or_else(|| KclError::refactor(format!("Object not found: {object_id:?}")))?;
5020 mutate_ast_node_by_source_ref(ast, &sketch_object.source, command)
5021 }
5022
5023 fn mutate_constraint_label_position(
5024 &mut self,
5025 ast: &mut ast::Node<ast::Program>,
5026 constraint_id: ObjectId,
5027 label_position: Point2d<Number>,
5028 ) -> Result<(), KclError> {
5029 let object = self
5030 .scene_graph
5031 .objects
5032 .get(constraint_id.0)
5033 .ok_or_else(|| KclError::refactor(format!("Object not found: {constraint_id:?}")))?;
5034 if !matches!(
5035 &object.kind,
5036 ObjectKind::Constraint {
5037 constraint: Constraint::Distance(_)
5038 | Constraint::HorizontalDistance(_)
5039 | Constraint::VerticalDistance(_)
5040 | Constraint::Radius(_)
5041 | Constraint::Diameter(_)
5042 | Constraint::Angle(_),
5043 }
5044 ) {
5045 return Err(KclError::refactor(format!(
5046 "Object does not support labelPosition: {constraint_id:?}"
5047 )));
5048 }
5049
5050 let label_position = to_ast_point2d_number(&label_position)
5051 .map_err(|err| KclError::refactor(format!("Could not convert label position to AST: {err}")))?;
5052 self.mutate_ast(
5053 ast,
5054 constraint_id,
5055 AstMutateCommand::EditDistanceConstraintLabelPosition { label_position },
5056 )?;
5057 Ok(())
5058 }
5059}
5060
5061fn sketch_block_ref_from_id(scene_graph: &SceneGraph, sketch_id: ObjectId) -> Result<AstNodeRef, KclError> {
5062 let sketch_object = scene_graph
5064 .objects
5065 .get(sketch_id.0)
5066 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch_id:?}")))?;
5067 let ObjectKind::Sketch(_) = &sketch_object.kind else {
5068 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
5069 };
5070 expect_single_node_ref(sketch_object)
5071}
5072
5073fn expect_single_node_ref(object: &Object) -> Result<AstNodeRef, KclError> {
5074 match &object.source {
5075 SourceRef::Simple { range, node_path } => Ok(AstNodeRef {
5076 range: *range,
5077 node_path: node_path.clone(),
5078 }),
5079 SourceRef::BackTrace { ranges } => {
5080 let [range] = ranges.as_slice() else {
5081 return Err(KclError::refactor(format!(
5082 "Expected single location in SourceRef, got {}; ranges={ranges:#?}",
5083 ranges.len()
5084 )));
5085 };
5086 Ok(AstNodeRef {
5087 range: range.0,
5088 node_path: range.1.clone(),
5089 })
5090 }
5091 }
5092}
5093
5094fn only_sketch_block_from_range(
5097 ast: &mut ast::Node<ast::Program>,
5098 sketch_block_range: SourceRange,
5099 edit_kind: ChangeKind,
5100) -> Result<(), KclError> {
5101 let r1 = sketch_block_range;
5102 let matches_range = |r2: SourceRange| -> bool {
5103 match edit_kind {
5106 ChangeKind::Add => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() <= r2.end(),
5107 ChangeKind::Edit => r1.module_id() == r2.module_id() && r1.start() == r2.start(),
5109 ChangeKind::Delete => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() >= r2.end(),
5110 ChangeKind::None => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() == r2.end(),
5112 }
5113 };
5114 let mut found = false;
5115 for item in ast.body.iter_mut() {
5116 match item {
5117 ast::BodyItem::ImportStatement(_) => {}
5118 ast::BodyItem::ExpressionStatement(node) => {
5119 if matches_range(SourceRange::from(&*node))
5120 && let ast::Expr::SketchBlock(sketch_block) = &mut node.expression
5121 {
5122 sketch_block.is_being_edited = true;
5123 found = true;
5124 break;
5125 }
5126 }
5127 ast::BodyItem::VariableDeclaration(node) => {
5128 if matches_range(SourceRange::from(&node.declaration.init))
5129 && let ast::Expr::SketchBlock(sketch_block) = &mut node.declaration.init
5130 {
5131 sketch_block.is_being_edited = true;
5132 found = true;
5133 break;
5134 }
5135 }
5136 ast::BodyItem::TypeDeclaration(_) => {}
5137 ast::BodyItem::ReturnStatement(node) => {
5138 if matches_range(SourceRange::from(&node.argument))
5139 && let ast::Expr::SketchBlock(sketch_block) = &mut node.argument
5140 {
5141 sketch_block.is_being_edited = true;
5142 found = true;
5143 break;
5144 }
5145 }
5146 }
5147 }
5148 if !found {
5149 return Err(KclError::refactor(format!(
5150 "Sketch block source range not found in AST: {sketch_block_range:?}, edit_kind={edit_kind:?}"
5151 )));
5152 }
5153
5154 Ok(())
5155}
5156
5157fn only_sketch_block(
5158 ast: &mut ast::Node<ast::Program>,
5159 sketch_block_ref: &AstNodeRef,
5160 edit_kind: ChangeKind,
5161) -> Result<(), KclError> {
5162 let Some(target_node_path) = &sketch_block_ref.node_path else {
5163 #[cfg(target_arch = "wasm32")]
5164 web_sys::console::warn_1(
5165 &format!(
5166 "only_sketch_block: target sketch block ref doesn't have node path; sketch_block_ref={:#?}, edit_kind={edit_kind:#?}",
5167 sketch_block_ref
5168 )
5169 .into(),
5170 );
5171 return only_sketch_block_from_range(ast, sketch_block_ref.range, edit_kind);
5172 };
5173 struct MarkSketchBlockBeingEdited<'a> {
5174 target_node_path: &'a ast::NodePath,
5175 }
5176
5177 impl Visitor for MarkSketchBlockBeingEdited<'_> {
5178 type Break = ();
5179 type Continue = ();
5180
5181 fn visit(&mut self, node: NodeMut<'_>) -> TraversalReturn<Self::Break, Self::Continue> {
5182 if let NodeMut::SketchBlock(sketch_block) = node
5183 && sketch_block.node_path.as_ref() == Some(self.target_node_path)
5184 {
5185 sketch_block.is_being_edited = true;
5186 return TraversalReturn::new_break(());
5187 }
5188 TraversalReturn::new_continue(())
5189 }
5190
5191 fn finish(&mut self, _node: NodeMut<'_>) {}
5192 }
5193
5194 let mut marker = MarkSketchBlockBeingEdited { target_node_path };
5195 let found = dfs_mut(ast, &mut marker).is_break();
5196 if !found {
5197 return Err(KclError::refactor(format!(
5198 "Sketch block node path not found in AST: {sketch_block_ref:?}, edit_kind={edit_kind:?}"
5199 )));
5200 }
5201
5202 Ok(())
5203}
5204
5205fn sketch_on_ast_expr(
5206 ast: &mut ast::Node<ast::Program>,
5207 scene_graph: &SceneGraph,
5208 solid_references: &HashMap<Uuid, SolidAstReference>,
5209 on: &Plane,
5210) -> Result<ast::Expr, KclError> {
5211 match on {
5212 Plane::Default(name) => Ok(default_plane_ast_expr(*name)),
5213 Plane::Object(object_id) => {
5214 let on_object = scene_graph
5215 .objects
5216 .get(object_id.0)
5217 .ok_or_else(|| KclError::refactor(format!("Sketch plane object not found: {object_id:?}")))?;
5218 if let Some(face_expr) = sketch_face_of_scene_object_ast_expr(ast, on_object)? {
5219 return Ok(face_expr);
5220 }
5221 get_or_insert_ast_reference(ast, &on_object.source, "plane", None)
5222 }
5223 Plane::PrimitiveFace(face) => {
5224 let solid_expr = solid_expr_for_engine_id(solid_references, face.solid_id).ok_or_else(|| {
5225 KclError::refactor(format!(
5226 "Could not resolve a KCL solid for selected primitive face: solid_id={}",
5227 face.solid_id
5228 ))
5229 })?;
5230 let face_id_expr = create_face_id_ast(solid_expr.clone(), face.index);
5231 Ok(create_face_of_ast(solid_expr, face_id_expr))
5232 }
5233 }
5234}
5235
5236fn solid_references_from_variables(
5237 ast: &ast::Node<ast::Program>,
5238 variables: &IndexMap<String, KclValueView>,
5239) -> HashMap<Uuid, SolidAstReference> {
5240 let mut references = HashMap::new();
5241
5242 for item in &ast.body {
5245 let ast::BodyItem::VariableDeclaration(declaration) = item else {
5246 continue;
5247 };
5248 let name = &declaration.declaration.id.name;
5249 let Some(value) = variables.get(name) else {
5250 continue;
5251 };
5252
5253 match value {
5254 KclValueView::Solid { value } => {
5255 references.insert(
5256 value.id,
5257 SolidAstReference {
5258 variable_name: name.clone(),
5259 output_index: None,
5260 },
5261 );
5262 }
5263 KclValueView::Tuple { value } | KclValueView::HomArray { value } => {
5264 for (output_index, entry) in value.iter().enumerate() {
5265 if let KclValueView::Solid { value } = entry {
5266 references.insert(
5267 value.id,
5268 SolidAstReference {
5269 variable_name: name.clone(),
5270 output_index: Some(output_index),
5271 },
5272 );
5273 }
5274 }
5275 }
5276 _ => {}
5277 }
5278 }
5279
5280 references
5281}
5282
5283fn solid_expr_for_engine_id(solid_references: &HashMap<Uuid, SolidAstReference>, solid_id: Uuid) -> Option<ast::Expr> {
5284 let reference = solid_references.get(&solid_id)?;
5285 let solid_expr = ast_name_expr(reference.variable_name.clone());
5286 Some(indexed_solid_expr_for_sweep_output(solid_expr, reference.output_index))
5287}
5288
5289fn sketch_face_of_scene_object_ast_expr(
5290 ast: &mut ast::Node<ast::Program>,
5291 on_object: &kcl_api::Object,
5292) -> Result<Option<ast::Expr>, KclError> {
5293 match &on_object.kind {
5294 ObjectKind::Wall(wall) => {
5295 let solid_ref = get_or_insert_ast_reference(
5296 ast,
5297 &source_ref_from_source_ref_range(&wall.source.solid),
5298 "solid",
5299 None,
5300 )?;
5301 let ast::Expr::Name(solid_name_expr) = solid_ref else {
5302 return Err(KclError::refactor(format!(
5303 "Could not resolve solid reference for selected wall: artifact_id={:?}",
5304 on_object.artifact_id
5305 )));
5306 };
5307 let solid_expr = indexed_solid_expr_for_sweep_output(
5308 ast_name_expr(solid_name_expr.name.name.clone()),
5309 wall.solid_output_index,
5310 );
5311 let sweep_ref = get_or_insert_ast_reference(
5312 ast,
5313 &source_ref_from_source_ref_range(&wall.source.sweep),
5314 "solid",
5315 None,
5316 )?;
5317 let ast::Expr::Name(sweep_name_expr) = sweep_ref else {
5318 return Err(KclError::refactor(format!(
5319 "Could not resolve sweep reference for selected wall: artifact_id={:?}",
5320 on_object.artifact_id
5321 )));
5322 };
5323 let sweep_name = sweep_name_expr.name.name.clone();
5324 let segment_ref = get_or_insert_ast_reference(
5325 ast,
5326 &source_ref_from_source_ref_range(&wall.source.segment),
5327 LINE_VARIABLE,
5328 None,
5329 )?;
5330
5331 let face_expr = if let Some(region_name) = region_name_from_sweep_variable(ast, &sweep_name).or_else(|| {
5332 wall.source
5333 .path
5334 .as_ref()
5335 .and_then(|path_source| region_name_from_path_source(ast, path_source))
5336 }) {
5337 let ast::Expr::Name(segment_name_expr) = segment_ref else {
5338 return Err(KclError::refactor(format!(
5339 "Could not resolve source segment reference for selected region wall: artifact_id={:?}",
5340 on_object.artifact_id
5341 )));
5342 };
5343 create_member_expression(
5344 create_member_expression(ast_name_expr(region_name), "tags"),
5345 &segment_name_expr.name.name,
5346 )
5347 } else {
5348 segment_ref
5349 };
5350
5351 Ok(Some(create_face_of_ast(solid_expr, face_expr)))
5352 }
5353 ObjectKind::Cap(cap) => {
5354 let solid_ref =
5355 get_or_insert_ast_reference(ast, &source_ref_from_source_ref_range(&cap.source.solid), "solid", None)?;
5356 let ast::Expr::Name(solid_name_expr) = solid_ref else {
5357 return Err(KclError::refactor(format!(
5358 "Could not resolve solid reference for selected cap: artifact_id={:?}",
5359 on_object.artifact_id
5360 )));
5361 };
5362 let solid_expr = indexed_solid_expr_for_sweep_output(
5363 ast_name_expr(solid_name_expr.name.name.clone()),
5364 cap.solid_output_index,
5365 );
5366 let face_expr = match cap.kind {
5368 kcl_api::CapKind::Start => ast_name_expr("START".to_owned()),
5369 kcl_api::CapKind::End => ast_name_expr("END".to_owned()),
5370 };
5371
5372 Ok(Some(create_face_of_ast(solid_expr, face_expr)))
5373 }
5374 _ => Ok(None),
5375 }
5376}
5377
5378fn indexed_solid_expr_for_sweep_output(solid_expr: ast::Expr, solid_output_index: Option<usize>) -> ast::Expr {
5379 match solid_output_index {
5380 Some(output_index) => create_index_expression(solid_expr, output_index),
5381 None => solid_expr,
5382 }
5383}
5384
5385fn source_ref_from_source_ref_range(source: &SourceRefRange) -> SourceRef {
5386 SourceRef::Simple {
5387 range: source.range,
5388 node_path: source.node_path.clone(),
5389 }
5390}
5391
5392fn region_name_from_path_source(ast: &ast::Node<ast::Program>, path_source: &SourceRefRange) -> Option<String> {
5393 let source_ref = source_ref_from_source_ref_range(path_source);
5394 let candidate = variable_name_containing_source_ref(ast, &source_ref)?;
5395 let ast::Definition::Variable(region_decl) = ast.get_variable(&candidate)? else {
5396 return None;
5397 };
5398 let ast::Expr::CallExpressionKw(region_call) = ®ion_decl.init else {
5399 return None;
5400 };
5401 if region_call.callee.name.name != "region" {
5402 return None;
5403 }
5404 Some(candidate)
5405}
5406
5407fn downstream_composite_code_ref_for_source(artifact_graph: &ArtifactGraph, source_id: ArtifactId) -> Option<&CodeRef> {
5408 let mut current_id = source_id;
5409 let mut current_composite = None;
5410 let mut visited = HashSet::new();
5411
5412 while visited.insert(current_id) {
5413 let next_composite_id = downstream_composite_id_for_solid_source(artifact_graph, current_id);
5414
5415 let Some(composite_id) = next_composite_id else {
5416 break;
5417 };
5418 let Some(Artifact::CompositeSolid(composite)) = artifact_graph.get(&composite_id) else {
5419 break;
5420 };
5421
5422 current_id = composite.id;
5423 current_composite = Some(composite);
5424
5425 if !composite.consumed {
5426 break;
5427 }
5428 }
5429
5430 current_composite.map(|composite| &composite.code_ref)
5431}
5432
5433fn downstream_composite_id_for_solid_source(
5434 artifact_graph: &ArtifactGraph,
5435 source_id: ArtifactId,
5436) -> Option<ArtifactId> {
5437 if let Some(Artifact::Path(path)) = artifact_graph.get(&source_id)
5439 && let Some(composite_id) = path.composite_solid_id
5440 && let Some(Artifact::CompositeSolid(composite)) = artifact_graph.get(&composite_id)
5441 && composite_contains_path_input(&composite.solid_ids, &composite.tool_ids, path.id, path.solid2d_id)
5442 {
5443 return Some(composite_id);
5444 }
5445
5446 for artifact in artifact_graph.values() {
5448 if let Artifact::Path(path) = artifact
5449 && path.sweep_id == Some(source_id)
5450 && let Some(composite_id) = path.composite_solid_id
5451 && let Some(Artifact::CompositeSolid(composite)) = artifact_graph.get(&composite_id)
5452 && composite_contains_path_input(&composite.solid_ids, &composite.tool_ids, path.id, path.solid2d_id)
5453 {
5454 return Some(composite_id);
5455 }
5456 }
5457
5458 artifact_graph.values().find_map(|artifact| {
5460 let Artifact::CompositeSolid(composite) = artifact else {
5461 return None;
5462 };
5463 composite_contains_input(&composite.solid_ids, &composite.tool_ids, source_id).then_some(composite.id)
5464 })
5465}
5466
5467fn composite_contains_path_input(
5468 solid_ids: &[ArtifactId],
5469 tool_ids: &[ArtifactId],
5470 path_id: ArtifactId,
5471 solid2d_id: Option<ArtifactId>,
5472) -> bool {
5473 composite_contains_input(solid_ids, tool_ids, path_id)
5474 || solid2d_id.is_some_and(|solid2d_id| composite_contains_input(solid_ids, tool_ids, solid2d_id))
5475}
5476
5477fn composite_contains_input(solid_ids: &[ArtifactId], tool_ids: &[ArtifactId], input_id: ArtifactId) -> bool {
5478 solid_ids.contains(&input_id) || tool_ids.contains(&input_id)
5479}
5480
5481fn code_ref_source_ref_range(code_ref: &CodeRef) -> SourceRefRange {
5482 let node_path = (!code_ref.node_path.is_empty()).then(|| code_ref.node_path.clone());
5483 SourceRefRange {
5484 range: code_ref.range,
5485 node_path,
5486 }
5487}
5488
5489fn solid_output_index_for_sweep(
5490 artifact_graph: &ArtifactGraph,
5491 sweep_id: ArtifactId,
5492 sweep_code_ref: &CodeRef,
5493) -> Option<usize> {
5494 if downstream_composite_id_for_solid_source(artifact_graph, sweep_id).is_some() {
5497 return None;
5498 }
5499
5500 let sibling_sweeps = artifact_graph
5501 .values()
5502 .filter_map(|artifact| match artifact {
5503 Artifact::Sweep(sweep)
5504 if sweep.code_ref.range == sweep_code_ref.range
5505 && sweep.code_ref.node_path == sweep_code_ref.node_path =>
5506 {
5507 Some(sweep)
5508 }
5509 _ => None,
5510 })
5511 .collect::<Vec<_>>();
5512
5513 if sibling_sweeps.len() <= 1 {
5514 return None;
5515 }
5516
5517 sibling_sweeps
5518 .iter()
5519 .position(|sibling_sweep| sibling_sweep.id == sweep_id)
5520}
5521
5522fn add_wall_and_cap_face_objects(scene_objects: &mut Vec<kcl_api::Object>, artifact_graph: &ArtifactGraph) {
5523 let mut existing_artifact_ids = scene_objects
5524 .iter()
5525 .map(|object| object.artifact_id)
5526 .collect::<HashSet<_>>();
5527
5528 for artifact in artifact_graph.values() {
5529 match artifact {
5530 Artifact::Wall(wall) => {
5531 if existing_artifact_ids.contains(&wall.id) {
5532 continue;
5533 }
5534
5535 let Some(segment) = artifact_graph.get(&wall.seg_id).and_then(|artifact| match artifact {
5536 Artifact::Segment(segment) => Some(segment),
5537 _ => None,
5538 }) else {
5539 continue;
5540 };
5541 let Some(sweep) = artifact_graph.get(&wall.sweep_id).and_then(|artifact| match artifact {
5542 Artifact::Sweep(sweep) => Some(sweep),
5543 _ => None,
5544 }) else {
5545 continue;
5546 };
5547 let source_segment = segment
5548 .original_seg_id
5549 .and_then(|original_seg_id| artifact_graph.get(&original_seg_id))
5550 .and_then(|artifact| match artifact {
5551 Artifact::Segment(segment) => Some(segment),
5552 _ => None,
5553 })
5554 .unwrap_or(segment);
5555 let solid_code_ref =
5556 downstream_composite_code_ref_for_source(artifact_graph, wall.sweep_id).unwrap_or(&sweep.code_ref);
5557 let path_code_ref = artifact_graph
5558 .get(&segment.path_id)
5559 .or_else(|| sweep.path_id.and_then(|path_id| artifact_graph.get(&path_id)))
5560 .and_then(|artifact| match artifact {
5561 Artifact::Path(path) => Some(&path.code_ref),
5562 _ => None,
5563 });
5564 let source = WallSource {
5565 solid: code_ref_source_ref_range(solid_code_ref),
5566 sweep: code_ref_source_ref_range(&sweep.code_ref),
5567 path: path_code_ref.map(code_ref_source_ref_range),
5568 segment: code_ref_source_ref_range(&source_segment.code_ref),
5569 };
5570 let solid_output_index = (solid_code_ref.range == sweep.code_ref.range
5571 && solid_code_ref.node_path == sweep.code_ref.node_path)
5572 .then(|| solid_output_index_for_sweep(artifact_graph, sweep.id, &sweep.code_ref))
5573 .flatten();
5574 let object_source = source_ref_from_source_ref_range(&source.solid);
5575 let id = ObjectId(scene_objects.len());
5576 scene_objects.push(kcl_api::Object {
5577 id,
5578 kind: ObjectKind::Wall(kcl_api::Wall {
5579 id,
5580 source,
5581 solid_output_index,
5582 }),
5583 label: Default::default(),
5584 comments: Default::default(),
5585 artifact_id: wall.id,
5586 source: object_source,
5587 });
5588 existing_artifact_ids.insert(wall.id);
5589 }
5590 Artifact::Cap(cap) => {
5591 if existing_artifact_ids.contains(&cap.id) {
5592 continue;
5593 }
5594
5595 let Some(sweep) = artifact_graph.get(&cap.sweep_id).and_then(|artifact| match artifact {
5596 Artifact::Sweep(sweep) => Some(sweep),
5597 _ => None,
5598 }) else {
5599 continue;
5600 };
5601 let id = ObjectId(scene_objects.len());
5602 let kind = match cap.sub_type {
5603 CapSubType::Start => kcl_api::CapKind::Start,
5604 CapSubType::End => kcl_api::CapKind::End,
5605 };
5606 let solid_code_ref =
5607 downstream_composite_code_ref_for_source(artifact_graph, cap.sweep_id).unwrap_or(&sweep.code_ref);
5608 let source = CapSource {
5609 solid: code_ref_source_ref_range(solid_code_ref),
5610 sweep: code_ref_source_ref_range(&sweep.code_ref),
5611 };
5612 let solid_output_index = (solid_code_ref.range == sweep.code_ref.range
5613 && solid_code_ref.node_path == sweep.code_ref.node_path)
5614 .then(|| solid_output_index_for_sweep(artifact_graph, sweep.id, &sweep.code_ref))
5615 .flatten();
5616 let object_source = source_ref_from_source_ref_range(&source.solid);
5617 scene_objects.push(kcl_api::Object {
5618 id,
5619 kind: ObjectKind::Cap(kcl_api::Cap {
5620 id,
5621 kind,
5622 source,
5623 solid_output_index,
5624 }),
5625 label: Default::default(),
5626 comments: Default::default(),
5627 artifact_id: cap.id,
5628 source: object_source,
5629 });
5630 existing_artifact_ids.insert(cap.id);
5631 }
5632 _ => {}
5633 }
5634 }
5635}
5636
5637fn default_plane_ast_expr(name: crate::engine::PlaneName) -> ast::Expr {
5638 use crate::engine::PlaneName;
5639
5640 match name {
5641 PlaneName::Xy => ast_name_expr("XY".to_owned()),
5642 PlaneName::Xz => ast_name_expr("XZ".to_owned()),
5643 PlaneName::Yz => ast_name_expr("YZ".to_owned()),
5644 PlaneName::NegXy => negated_plane_ast_expr("XY"),
5645 PlaneName::NegXz => negated_plane_ast_expr("XZ"),
5646 PlaneName::NegYz => negated_plane_ast_expr("YZ"),
5647 }
5648}
5649
5650fn negated_plane_ast_expr(name: &str) -> ast::Expr {
5651 ast::Expr::UnaryExpression(BoxNode::new(ast::UnaryExpression::new(
5652 ast::UnaryOperator::Neg,
5653 ast::BinaryPart::Name(BoxNode::new(ast_name(name.to_owned()))),
5654 )))
5655}
5656
5657fn create_face_of_ast(solid_expr: ast::Expr, face_expr: ast::Expr) -> ast::Expr {
5658 ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
5659 callee: ast::Node::no_src(ast_sketch2_name("faceOf")),
5660 unlabeled: Some(solid_expr),
5661 arguments: vec![ast::LabeledArg {
5662 label: Some(ast::Identifier::new("face")),
5663 arg: face_expr,
5664 }],
5665 digest: None,
5666 non_code_meta: Default::default(),
5667 })))
5668}
5669
5670fn create_face_id_ast(solid_expr: ast::Expr, index: usize) -> ast::Expr {
5671 ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
5672 callee: ast::Node::no_src(ast_sketch2_name("faceId")),
5673 unlabeled: Some(solid_expr),
5674 arguments: vec![ast::LabeledArg {
5675 label: Some(ast::Identifier::new("index")),
5676 arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal::from(
5677 ast::NumericLiteral {
5678 value: index as f64,
5679 suffix: NumericSuffix::None,
5680 raw: index.to_string(),
5681 digest: None,
5682 },
5683 )))),
5684 }],
5685 digest: None,
5686 non_code_meta: Default::default(),
5687 })))
5688}
5689
5690fn region_name_from_sweep_variable(ast: &ast::Node<ast::Program>, sweep_variable_name: &str) -> Option<String> {
5691 let ast::Definition::Variable(sweep_decl) = ast.get_variable(sweep_variable_name)? else {
5692 return None;
5693 };
5694 let ast::Expr::CallExpressionKw(sweep_call) = &sweep_decl.init else {
5695 return None;
5696 };
5697 if !matches!(
5698 sweep_call.callee.name.name.as_str(),
5699 "extrude" | "revolve" | "sweep" | "loft"
5700 ) {
5701 return None;
5702 }
5703 let ast::Expr::Name(region_name_expr) = sweep_call.unlabeled.as_ref()? else {
5704 return None;
5705 };
5706 let candidate = region_name_expr.name.name.clone();
5707 let ast::Definition::Variable(region_decl) = ast.get_variable(&candidate)? else {
5708 return None;
5709 };
5710 let ast::Expr::CallExpressionKw(region_call) = ®ion_decl.init else {
5711 return None;
5712 };
5713 if region_call.callee.name.name != "region" {
5714 return None;
5715 }
5716 Some(candidate)
5717}
5718
5719fn get_or_insert_ast_reference(
5726 ast: &mut ast::Node<ast::Program>,
5727 source_ref: &SourceRef,
5728 prefix: &str,
5729 property: Option<&str>,
5730) -> Result<ast::Expr, KclError> {
5731 let command = AstMutateCommand::AddVariableDeclaration {
5732 prefix: prefix.to_owned(),
5733 };
5734 let ret = match mutate_ast_node_by_source_ref(ast, source_ref, command) {
5735 Ok((_, ret)) => ret,
5736 Err(err) => {
5737 if let Some(var_name) = variable_name_containing_source_ref(ast, source_ref) {
5738 AstMutateCommandReturn::Name(var_name)
5739 } else {
5740 return Err(err);
5741 }
5742 }
5743 };
5744 let AstMutateCommandReturn::Name(var_name) = ret else {
5745 return Err(KclError::refactor(
5746 "Expected variable name returned from AddVariableDeclaration".to_owned(),
5747 ));
5748 };
5749 let var_expr = ast::Expr::Name(BoxNode::new(ast::Name::new(&var_name)));
5750 let Some(property) = property else {
5751 return Ok(var_expr);
5753 };
5754
5755 Ok(create_member_expression(var_expr, property))
5756}
5757
5758fn variable_name_containing_source_ref(ast: &ast::Node<ast::Program>, source_ref: &SourceRef) -> Option<String> {
5759 let source_range = match source_ref {
5760 SourceRef::Simple { range, .. } => *range,
5761 SourceRef::BackTrace { ranges } => {
5762 let [range] = ranges.as_slice() else {
5763 return None;
5764 };
5765 range.0
5766 }
5767 };
5768 ast.body.iter().find_map(|item| {
5769 let ast::BodyItem::VariableDeclaration(var_decl) = item else {
5770 return None;
5771 };
5772 let init_range = SourceRange::from(&var_decl.declaration.init);
5773 let source_is_inside_init = init_range.module_id() == source_range.module_id()
5774 && init_range.start() <= source_range.start()
5775 && source_range.end() <= init_range.end();
5776 if matches!(&var_decl.declaration.init, ast::Expr::SketchBlock(_))
5777 && init_range != source_range
5778 && source_is_inside_init
5779 {
5780 return None;
5781 }
5782 source_is_inside_init.then(|| var_decl.name().to_owned())
5783 })
5784}
5785
5786fn mutate_ast_node_by_source_ref(
5787 ast: &mut ast::Node<ast::Program>,
5788 source_ref: &SourceRef,
5789 command: AstMutateCommand,
5790) -> Result<(AstNodeRef, AstMutateCommandReturn), KclError> {
5791 let (source_range, node_path) = match source_ref {
5792 SourceRef::Simple { range, node_path } => (*range, node_path.clone()),
5793 SourceRef::BackTrace { ranges } => {
5794 let [range] = ranges.as_slice() else {
5795 return Err(KclError::refactor(format!(
5796 "Expected single source ref, got {}; ranges={ranges:#?}",
5797 ranges.len(),
5798 )));
5799 };
5800 (range.0, range.1.clone())
5801 }
5802 };
5803 let mut context = AstMutateContext {
5804 source_range,
5805 node_path,
5806 command,
5807 defined_names_stack: Default::default(),
5808 };
5809 let control = dfs_mut(ast, &mut context);
5810 match control {
5811 ControlFlow::Continue(_) => Err(KclError::refactor(
5812 "Could not find the KCL source for this edit. Try reloading the app, or update from code.".to_owned(),
5813 )),
5814 ControlFlow::Break(break_value) => break_value,
5815 }
5816}
5817
5818#[derive(Debug)]
5819struct AstMutateContext {
5820 source_range: SourceRange,
5821 node_path: Option<ast::NodePath>,
5822 command: AstMutateCommand,
5823 defined_names_stack: Vec<HashSet<String>>,
5824}
5825
5826#[derive(Debug)]
5827#[allow(clippy::large_enum_variant)]
5828enum AstMutateCommand {
5829 AddSketchBlockExprStmt {
5831 expr: ast::Expr,
5832 },
5833 AddSketchBlockVarDecl {
5835 prefix: String,
5836 expr: ast::Expr,
5837 },
5838 AddVariableDeclaration {
5839 prefix: String,
5840 },
5841 EditPoint {
5842 at: ast::Expr,
5843 },
5844 EditLine {
5845 start: ast::Expr,
5846 end: ast::Expr,
5847 construction: Option<bool>,
5848 },
5849 EditArc {
5850 start: ast::Expr,
5851 end: ast::Expr,
5852 center: ast::Expr,
5853 direction: Option<ArcDirection>,
5854 construction: Option<bool>,
5855 },
5856 EditCircle {
5857 start: ast::Expr,
5858 center: ast::Expr,
5859 construction: Option<bool>,
5860 },
5861 EditControlPointSpline {
5862 points: ast::Expr,
5863 construction: Option<bool>,
5864 },
5865 EditConstraintValue {
5866 value: ast::BinaryPart,
5867 },
5868 EditAngleConstraint {
5869 call: ast::BinaryPart,
5870 value: ast::BinaryPart,
5871 },
5872 EditDistanceConstraint {
5873 call: ast::BinaryPart,
5874 value: ast::BinaryPart,
5875 },
5876 EditDistanceConstraintLabelPosition {
5877 label_position: ast::Expr,
5878 },
5879 EditCallUnlabeled {
5880 arg: ast::Expr,
5881 },
5882 EditVarInitialValue {
5883 value: Number,
5884 },
5885 DeleteNode,
5886}
5887
5888impl AstMutateCommand {
5889 fn needs_defined_names_stack(&self) -> bool {
5890 matches!(
5891 self,
5892 AstMutateCommand::AddSketchBlockVarDecl { .. } | AstMutateCommand::AddVariableDeclaration { .. }
5893 )
5894 }
5895}
5896
5897#[derive(Debug)]
5898enum AstMutateCommandReturn {
5899 None,
5900 Name(String),
5901}
5902
5903#[derive(Debug, Clone)]
5904struct AstNodeRef {
5905 range: SourceRange,
5906 node_path: Option<ast::NodePath>,
5907}
5908
5909impl<T> From<&ast::Node<T>> for AstNodeRef {
5910 fn from(value: &ast::Node<T>) -> Self {
5911 AstNodeRef {
5912 range: value.into(),
5913 node_path: value.node_path.clone(),
5914 }
5915 }
5916}
5917
5918impl From<&ast::BodyItem> for AstNodeRef {
5919 fn from(value: &ast::BodyItem) -> Self {
5920 match value {
5921 ast::BodyItem::ImportStatement(node) => AstNodeRef {
5922 range: node.into(),
5923 node_path: node.node_path.clone(),
5924 },
5925 ast::BodyItem::ExpressionStatement(node) => AstNodeRef {
5926 range: node.into(),
5927 node_path: node.node_path.clone(),
5928 },
5929 ast::BodyItem::VariableDeclaration(node) => AstNodeRef {
5930 range: node.into(),
5931 node_path: node.node_path.clone(),
5932 },
5933 ast::BodyItem::TypeDeclaration(node) => AstNodeRef {
5934 range: node.into(),
5935 node_path: node.node_path.clone(),
5936 },
5937 ast::BodyItem::ReturnStatement(node) => AstNodeRef {
5938 range: node.into(),
5939 node_path: node.node_path.clone(),
5940 },
5941 }
5942 }
5943}
5944
5945impl From<&ast::Expr> for AstNodeRef {
5946 fn from(value: &ast::Expr) -> Self {
5947 AstNodeRef {
5948 range: SourceRange::from(value),
5949 node_path: value.node_path().cloned(),
5950 }
5951 }
5952}
5953
5954impl From<&AstMutateContext> for AstNodeRef {
5955 fn from(value: &AstMutateContext) -> Self {
5956 AstNodeRef {
5957 range: value.source_range,
5958 node_path: value.node_path.clone(),
5959 }
5960 }
5961}
5962
5963impl TryFrom<&NodeMut<'_>> for AstNodeRef {
5964 type Error = crate::walk::AstNodeError;
5965
5966 fn try_from(value: &NodeMut<'_>) -> Result<Self, Self::Error> {
5967 Ok(AstNodeRef {
5968 range: SourceRange::try_from(value)?,
5969 node_path: value.try_into()?,
5970 })
5971 }
5972}
5973
5974impl From<AstNodeRef> for SourceRange {
5975 fn from(value: AstNodeRef) -> Self {
5976 value.range
5977 }
5978}
5979
5980impl Visitor for AstMutateContext {
5981 type Break = Result<(AstNodeRef, AstMutateCommandReturn), KclError>;
5982 type Continue = ();
5983
5984 fn visit(&mut self, node: NodeMut<'_>) -> TraversalReturn<Self::Break, Self::Continue> {
5985 filter_and_process(self, node)
5986 }
5987
5988 fn finish(&mut self, node: NodeMut<'_>) {
5989 match &node {
5990 NodeMut::Program(_) | NodeMut::SketchBlock(_) => {
5991 self.defined_names_stack.pop();
5992 }
5993 _ => {}
5994 }
5995 }
5996}
5997
5998fn filter_and_process(
5999 ctx: &mut AstMutateContext,
6000 node: NodeMut,
6001) -> TraversalReturn<Result<(AstNodeRef, AstMutateCommandReturn), KclError>> {
6002 let Ok(node_range) = SourceRange::try_from(&node) else {
6003 return TraversalReturn::new_continue(());
6005 };
6006 if let NodeMut::VariableDeclaration(var_decl) = &node {
6011 let expr_range = SourceRange::from(&var_decl.declaration.init);
6012 let expr_node_path = var_decl.declaration.init.node_path();
6013 if source_ref_matches(ctx, expr_range, expr_node_path) {
6014 if let AstMutateCommand::AddVariableDeclaration { .. } = &ctx.command {
6015 return TraversalReturn::new_break(Ok((
6018 AstNodeRef::from(&**var_decl),
6019 AstMutateCommandReturn::Name(var_decl.name().to_owned()),
6020 )));
6021 }
6022 if let AstMutateCommand::DeleteNode = &ctx.command {
6023 return TraversalReturn {
6026 mutate_body_item: MutateBodyItem::Delete,
6027 control_flow: ControlFlow::Break(Ok((AstNodeRef::from(&*ctx), AstMutateCommandReturn::None))),
6028 };
6029 }
6030 }
6031 }
6032 if let NodeMut::ExpressionStatement(expr_stmt) = &node {
6035 let expr_range = SourceRange::from(&expr_stmt.expression);
6036 let expr_node_path = expr_stmt.expression.node_path();
6037 if source_ref_matches(ctx, expr_range, expr_node_path) {
6038 if let AstMutateCommand::AddVariableDeclaration { .. } = &ctx.command {
6039 let Ok(node_ref) = AstNodeRef::try_from(&node) else {
6042 return TraversalReturn::new_continue(());
6043 };
6044 return process(ctx, node).map_break(|result| result.map(|cmd_return| (node_ref, cmd_return)));
6045 }
6046 if let AstMutateCommand::DeleteNode = &ctx.command {
6047 return TraversalReturn {
6050 mutate_body_item: MutateBodyItem::Delete,
6051 control_flow: ControlFlow::Break(Ok((AstNodeRef::from(&*ctx), AstMutateCommandReturn::None))),
6052 };
6053 }
6054 }
6055 }
6056
6057 if ctx.command.needs_defined_names_stack() {
6058 if let NodeMut::Program(program) = &node {
6059 ctx.defined_names_stack.push(find_defined_names(*program));
6060 } else if let NodeMut::SketchBlock(block) = &node {
6061 ctx.defined_names_stack.push(find_defined_names(&block.body));
6062 }
6063 }
6064
6065 let node_path = <Option<ast::NodePath>>::try_from(&node).ok().flatten();
6067 if !source_ref_matches(ctx, node_range, node_path.as_ref()) {
6068 return TraversalReturn::new_continue(());
6069 }
6070 let Ok(node_ref) = AstNodeRef::try_from(&node) else {
6071 return TraversalReturn::new_continue(());
6072 };
6073 process(ctx, node).map_break(|result| result.map(|cmd_return| (node_ref, cmd_return)))
6074}
6075
6076fn source_ref_matches(ctx: &AstMutateContext, node_range: SourceRange, node_path: Option<&ast::NodePath>) -> bool {
6077 match &ctx.node_path {
6078 Some(target) => Some(target) == node_path,
6079 None => node_range == ctx.source_range,
6080 }
6081}
6082
6083fn is_angle_constraint_call_name(name: &str) -> bool {
6084 matches!(name, ANGLE_FN | ANGLE_DIMENSION_FN)
6085}
6086
6087fn is_distance_constraint_call_name(name: &str) -> bool {
6088 matches!(name, DISTANCE_FN | HORIZONTAL_DISTANCE_FN | VERTICAL_DISTANCE_FN)
6089}
6090
6091fn is_constraint_call_name(name: &str) -> bool {
6092 matches!(
6093 name,
6094 DISTANCE_FN
6095 | HORIZONTAL_DISTANCE_FN
6096 | VERTICAL_DISTANCE_FN
6097 | RADIUS_FN
6098 | DIAMETER_FN
6099 | ANGLE_FN
6100 | ANGLE_DIMENSION_FN
6101 )
6102}
6103
6104fn constraint_supports_label_position(part: &mut ast::BinaryPart) -> Option<&mut BoxNode<CallExpressionKw>> {
6105 if let ast::BinaryPart::CallExpressionKw(call) = part
6106 && is_constraint_call_name(call.callee.name.name.as_str())
6107 {
6108 Some(call)
6109 } else {
6110 None
6111 }
6112}
6113
6114fn process(ctx: &AstMutateContext, node: NodeMut) -> TraversalReturn<Result<AstMutateCommandReturn, KclError>> {
6115 match &ctx.command {
6116 AstMutateCommand::AddSketchBlockExprStmt { expr } => {
6117 if let NodeMut::SketchBlock(sketch_block) = node {
6118 sketch_block
6119 .body
6120 .items
6121 .push(ast::BodyItem::ExpressionStatement(ast::Node {
6122 inner: ast::ExpressionStatement {
6123 expression: expr.clone(),
6124 digest: None,
6125 },
6126 start: Default::default(),
6127 end: Default::default(),
6128 module_id: Default::default(),
6129 node_path: None,
6130 outer_attrs: Default::default(),
6131 pre_comments: Default::default(),
6132 comment_start: Default::default(),
6133 }));
6134 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6135 }
6136 }
6137 AstMutateCommand::AddSketchBlockVarDecl { prefix, expr } => {
6138 if let NodeMut::SketchBlock(sketch_block) = node {
6139 let empty_defined_names = HashSet::new();
6140 let defined_names = ctx.defined_names_stack.last().unwrap_or(&empty_defined_names);
6141 let Ok(name) = next_free_name(prefix, defined_names) else {
6142 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6143 };
6144 sketch_block
6145 .body
6146 .items
6147 .push(ast::BodyItem::VariableDeclaration(BoxNode::new(ast::Node::no_src(
6148 ast::VariableDeclaration::new(
6149 ast::VariableDeclarator::new(&name, expr.clone()),
6150 ast::ItemVisibility::Default,
6151 ast::VariableKind::Const,
6152 ),
6153 ))));
6154 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::Name(name)));
6155 }
6156 }
6157 AstMutateCommand::AddVariableDeclaration { prefix } => {
6158 if let NodeMut::VariableDeclaration(inner) = node {
6159 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::Name(inner.name().to_owned())));
6160 }
6161 if let NodeMut::ExpressionStatement(expr_stmt) = node {
6162 let empty_defined_names = HashSet::new();
6163 let defined_names = ctx.defined_names_stack.last().unwrap_or(&empty_defined_names);
6164 let Ok(name) = next_free_name(prefix, defined_names) else {
6165 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6167 };
6168 let mutate_node =
6169 ast::BodyItem::VariableDeclaration(BoxNode::new(ast::Node::no_src(ast::VariableDeclaration::new(
6170 ast::VariableDeclarator::new(&name, expr_stmt.expression.clone()),
6171 ast::ItemVisibility::Default,
6172 ast::VariableKind::Const,
6173 ))));
6174 return TraversalReturn {
6175 mutate_body_item: MutateBodyItem::Mutate(Box::new(mutate_node)),
6176 control_flow: ControlFlow::Break(Ok(AstMutateCommandReturn::Name(name))),
6177 };
6178 }
6179 }
6180 AstMutateCommand::EditPoint { at } => {
6181 if let NodeMut::CallExpressionKw(call) = node {
6182 if call.callee.name.name != POINT_FN {
6183 return TraversalReturn::new_continue(());
6184 }
6185 for labeled_arg in &mut call.arguments {
6187 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(POINT_AT_PARAM) {
6188 labeled_arg.arg = at.clone();
6189 }
6190 }
6191 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6192 }
6193 }
6194 AstMutateCommand::EditLine {
6195 start,
6196 end,
6197 construction,
6198 } => {
6199 if let NodeMut::CallExpressionKw(call) = node {
6200 if call.callee.name.name != LINE_FN {
6201 return TraversalReturn::new_continue(());
6202 }
6203 for labeled_arg in &mut call.arguments {
6205 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(LINE_START_PARAM) {
6206 labeled_arg.arg = start.clone();
6207 }
6208 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(LINE_END_PARAM) {
6209 labeled_arg.arg = end.clone();
6210 }
6211 }
6212 if let Some(construction_value) = construction {
6214 let construction_exists = call
6215 .arguments
6216 .iter()
6217 .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
6218 if *construction_value {
6219 if construction_exists {
6221 for labeled_arg in &mut call.arguments {
6223 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
6224 labeled_arg.arg =
6225 ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6226 value: ast::LiteralValue::Bool(true),
6227 raw: "true".to_string(),
6228 digest: None,
6229 })));
6230 }
6231 }
6232 } else {
6233 call.arguments.push(ast::LabeledArg {
6235 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
6236 arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6237 value: ast::LiteralValue::Bool(true),
6238 raw: "true".to_string(),
6239 digest: None,
6240 }))),
6241 });
6242 }
6243 } else {
6244 call.arguments
6246 .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
6247 }
6248 }
6249 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6250 }
6251 }
6252 AstMutateCommand::EditArc {
6253 start,
6254 end,
6255 center,
6256 direction,
6257 construction,
6258 } => {
6259 if let NodeMut::CallExpressionKw(call) = node {
6260 if call.callee.name.name != ARC_FN {
6261 return TraversalReturn::new_continue(());
6262 }
6263 for labeled_arg in &mut call.arguments {
6265 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_START_PARAM) {
6266 labeled_arg.arg = start.clone();
6267 }
6268 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_END_PARAM) {
6269 labeled_arg.arg = end.clone();
6270 }
6271 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_CENTER_PARAM) {
6272 labeled_arg.arg = center.clone();
6273 }
6274 }
6275 if let Some(direction_value) = direction {
6277 let direction_exists = call
6278 .arguments
6279 .iter()
6280 .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_DIRECTION_PARAM));
6281 if direction_value.is_clockwise() {
6282 let direction_ast = ast::Expr::Name(BoxNode::new(ast::Name::new(ARC_DIRECTION_CW_NAME)));
6283 if direction_exists {
6284 for labeled_arg in &mut call.arguments {
6286 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_DIRECTION_PARAM) {
6287 labeled_arg.arg = direction_ast.clone();
6288 }
6289 }
6290 } else {
6291 call.arguments.push(ast::LabeledArg {
6293 label: Some(ast::Identifier::new(ARC_DIRECTION_PARAM)),
6294 arg: direction_ast,
6295 });
6296 }
6297 } else {
6298 call.arguments
6301 .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(ARC_DIRECTION_PARAM));
6302 }
6303 }
6304 if let Some(construction_value) = construction {
6306 let construction_exists = call
6307 .arguments
6308 .iter()
6309 .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
6310 if *construction_value {
6311 if construction_exists {
6313 for labeled_arg in &mut call.arguments {
6315 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
6316 labeled_arg.arg =
6317 ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6318 value: ast::LiteralValue::Bool(true),
6319 raw: "true".to_string(),
6320 digest: None,
6321 })));
6322 }
6323 }
6324 } else {
6325 call.arguments.push(ast::LabeledArg {
6327 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
6328 arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6329 value: ast::LiteralValue::Bool(true),
6330 raw: "true".to_string(),
6331 digest: None,
6332 }))),
6333 });
6334 }
6335 } else {
6336 call.arguments
6338 .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
6339 }
6340 }
6341 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6342 }
6343 }
6344 AstMutateCommand::EditCircle {
6345 start,
6346 center,
6347 construction,
6348 } => {
6349 if let NodeMut::CallExpressionKw(call) = node {
6350 if call.callee.name.name != CIRCLE_FN {
6351 return TraversalReturn::new_continue(());
6352 }
6353 for labeled_arg in &mut call.arguments {
6355 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CIRCLE_START_PARAM) {
6356 labeled_arg.arg = start.clone();
6357 }
6358 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CIRCLE_CENTER_PARAM) {
6359 labeled_arg.arg = center.clone();
6360 }
6361 }
6362 if let Some(construction_value) = construction {
6364 let construction_exists = call
6365 .arguments
6366 .iter()
6367 .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
6368 if *construction_value {
6369 if construction_exists {
6370 for labeled_arg in &mut call.arguments {
6372 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
6373 labeled_arg.arg =
6374 ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6375 value: ast::LiteralValue::Bool(true),
6376 raw: "true".to_string(),
6377 digest: None,
6378 })));
6379 }
6380 }
6381 } else {
6382 call.arguments.push(ast::LabeledArg {
6384 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
6385 arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6386 value: ast::LiteralValue::Bool(true),
6387 raw: "true".to_string(),
6388 digest: None,
6389 }))),
6390 });
6391 }
6392 } else {
6393 call.arguments
6395 .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
6396 }
6397 }
6398 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6399 }
6400 }
6401 AstMutateCommand::EditControlPointSpline { points, construction } => {
6402 if let NodeMut::CallExpressionKw(call) = node {
6403 if call.callee.name.name != CONTROL_POINT_SPLINE_FN {
6404 return TraversalReturn::new_continue(());
6405 }
6406 for labeled_arg in &mut call.arguments {
6407 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONTROL_POINT_SPLINE_POINTS_PARAM)
6408 {
6409 labeled_arg.arg = points.clone();
6410 }
6411 }
6412 if let Some(construction_value) = construction {
6414 let construction_exists = call
6415 .arguments
6416 .iter()
6417 .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
6418 if *construction_value {
6419 if construction_exists {
6420 for labeled_arg in &mut call.arguments {
6421 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
6422 labeled_arg.arg =
6423 ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6424 value: ast::LiteralValue::Bool(true),
6425 raw: "true".to_string(),
6426 digest: None,
6427 })));
6428 }
6429 }
6430 } else {
6431 call.arguments.push(ast::LabeledArg {
6432 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
6433 arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6434 value: ast::LiteralValue::Bool(true),
6435 raw: "true".to_string(),
6436 digest: None,
6437 }))),
6438 });
6439 }
6440 } else {
6441 call.arguments
6442 .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
6443 }
6444 }
6445 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6446 }
6447 }
6448 AstMutateCommand::EditConstraintValue { value } => {
6449 if let NodeMut::BinaryExpression(binary_expr) = node {
6450 let left_is_constraint = matches!(
6451 &binary_expr.left,
6452 ast::BinaryPart::CallExpressionKw(call) if is_constraint_call_name(call.callee.name.name.as_str())
6453 );
6454 if left_is_constraint {
6455 binary_expr.right = value.clone();
6456 } else {
6457 binary_expr.left = value.clone();
6458 }
6459
6460 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6461 }
6462 }
6463 AstMutateCommand::EditAngleConstraint { call, value } => {
6464 if let NodeMut::BinaryExpression(binary_expr) = node {
6465 let left_is_angle = matches!(
6466 &binary_expr.left,
6467 ast::BinaryPart::CallExpressionKw(existing_call)
6468 if is_angle_constraint_call_name(existing_call.callee.name.name.as_str())
6469 );
6470 let right_is_angle = matches!(
6471 &binary_expr.right,
6472 ast::BinaryPart::CallExpressionKw(existing_call)
6473 if is_angle_constraint_call_name(existing_call.callee.name.name.as_str())
6474 );
6475
6476 match (left_is_angle, right_is_angle) {
6477 (true, _) => {
6478 binary_expr.left = call.clone();
6479 binary_expr.right = value.clone();
6480 }
6481 (false, true) => {
6482 binary_expr.left = value.clone();
6483 binary_expr.right = call.clone();
6484 }
6485 (false, false) => return TraversalReturn::new_continue(()),
6486 }
6487
6488 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6489 }
6490 }
6491 AstMutateCommand::EditDistanceConstraint { call, value } => {
6492 if let NodeMut::BinaryExpression(binary_expr) = node {
6493 let left_is_distance = matches!(
6494 &binary_expr.left,
6495 ast::BinaryPart::CallExpressionKw(existing_call)
6496 if is_distance_constraint_call_name(existing_call.callee.name.name.as_str())
6497 );
6498 let right_is_distance = matches!(
6499 &binary_expr.right,
6500 ast::BinaryPart::CallExpressionKw(existing_call)
6501 if is_distance_constraint_call_name(existing_call.callee.name.name.as_str())
6502 );
6503
6504 match (left_is_distance, right_is_distance) {
6505 (true, _) => {
6506 binary_expr.left = call.clone();
6507 binary_expr.right = value.clone();
6508 }
6509 (false, true) => {
6510 binary_expr.left = value.clone();
6511 binary_expr.right = call.clone();
6512 }
6513 (false, false) => return TraversalReturn::new_continue(()),
6514 }
6515
6516 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6517 }
6518 }
6519 AstMutateCommand::EditDistanceConstraintLabelPosition { label_position } => {
6520 if let NodeMut::BinaryExpression(binary_expr) = node {
6521 let call = if let Some(call) = constraint_supports_label_position(&mut binary_expr.left) {
6522 call
6523 } else if let Some(call) = constraint_supports_label_position(&mut binary_expr.right) {
6524 call
6525 } else {
6526 return TraversalReturn::new_continue(());
6527 };
6528
6529 if let Some(label_arg) = call
6530 .arguments
6531 .iter_mut()
6532 .find(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(LABEL_POSITION_PARAM))
6533 {
6534 label_arg.arg = label_position.clone();
6535 } else {
6536 call.arguments.push(ast::LabeledArg {
6537 label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
6538 arg: label_position.clone(),
6539 });
6540 }
6541
6542 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6543 }
6544 }
6545 AstMutateCommand::EditCallUnlabeled { arg } => {
6546 if let NodeMut::CallExpressionKw(call) = node {
6547 call.unlabeled = Some(arg.clone());
6548 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6549 }
6550 }
6551 AstMutateCommand::EditVarInitialValue { value } => {
6552 if let NodeMut::SketchVar(sketch_var) = node {
6556 let Ok(literal) = to_source_number(*value) else {
6557 return TraversalReturn::new_break(Err(KclError::refactor(format!(
6558 "Could not convert number to AST literal: {:?}",
6559 *value
6560 ))));
6561 };
6562 sketch_var.initial = Some(BoxNode::new(ast::Node::no_src(literal)));
6563 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6564 }
6565 }
6566 AstMutateCommand::DeleteNode => {
6567 return TraversalReturn {
6568 mutate_body_item: MutateBodyItem::Delete,
6569 control_flow: ControlFlow::Break(Ok(AstMutateCommandReturn::None)),
6570 };
6571 }
6572 }
6573 TraversalReturn::new_continue(())
6574}
6575
6576struct FindSketchBlockSourceRange {
6577 target_before_mutation: SourceRange,
6579 found: Cell<Option<AstNodeRef>>,
6583}
6584
6585impl<'a> crate::walk::Visitor<'a> for &FindSketchBlockSourceRange {
6586 type Error = crate::front::Error;
6587
6588 fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
6589 let Ok(node_range) = SourceRange::try_from(&node) else {
6590 return Ok(true);
6591 };
6592
6593 if let crate::walk::Node::SketchBlock(sketch_block) = node {
6594 if node_range.module_id() == self.target_before_mutation.module_id()
6595 && node_range.start() == self.target_before_mutation.start()
6596 && node_range.end() >= self.target_before_mutation.end()
6598 {
6599 self.found.set(sketch_block.body.items.last().map(|item| match item {
6600 ast::BodyItem::VariableDeclaration(node) => AstNodeRef::from(&node.declaration.init),
6604 _ => AstNodeRef::from(item),
6605 }));
6606 return Ok(false);
6607 } else {
6608 return Ok(true);
6611 }
6612 }
6613
6614 for child in node.children().iter() {
6615 if !child.visit(*self)? {
6616 return Ok(false);
6617 }
6618 }
6619
6620 Ok(true)
6621 }
6622}
6623
6624struct FindSketchBlockByNodePath {
6625 target_node_path: ast::NodePath,
6627 found: Cell<Option<AstNodeRef>>,
6631}
6632
6633impl<'a> crate::walk::Visitor<'a> for &FindSketchBlockByNodePath {
6634 type Error = crate::front::Error;
6635
6636 fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
6637 let Ok(node_path) = <Option<ast::NodePath>>::try_from(&node) else {
6638 return Ok(true);
6639 };
6640
6641 if let crate::walk::Node::SketchBlock(sketch_block) = node {
6642 if let Some(node_path) = node_path
6643 && node_path == self.target_node_path
6644 {
6645 self.found.set(sketch_block.body.items.last().map(|item| match item {
6646 ast::BodyItem::VariableDeclaration(node) => AstNodeRef::from(&node.declaration.init),
6650 _ => AstNodeRef::from(item),
6651 }));
6652
6653 return Ok(false);
6654 } else {
6655 return Ok(true);
6658 }
6659 }
6660
6661 for child in node.children().iter() {
6662 if !child.visit(*self)? {
6663 return Ok(false);
6664 }
6665 }
6666
6667 Ok(true)
6668 }
6669}
6670
6671fn find_sketch_block_added_item(
6679 ast: &ast::Node<ast::Program>,
6680 sketch_block_before_mutation: &AstNodeRef,
6681) -> Result<AstNodeRef, KclError> {
6682 if let Some(node_path) = &sketch_block_before_mutation.node_path {
6683 let find = FindSketchBlockByNodePath {
6684 target_node_path: node_path.clone(),
6685 found: Cell::new(None),
6686 };
6687 let node = crate::walk::Node::from(ast);
6688 node.visit(&find).map_err(|err| KclError::refactor(err.msg))?;
6689 find.found.into_inner().ok_or_else(|| {
6690 KclError::refactor(format!(
6691 "Node ID after mutation not found for Node ID before mutation: {node_path:?}"
6692 ))
6693 })
6694 } else {
6695 let find = FindSketchBlockSourceRange {
6697 target_before_mutation: sketch_block_before_mutation.range,
6698 found: Cell::new(None),
6699 };
6700 let node = crate::walk::Node::from(ast);
6701 node.visit(&find).map_err(|err| KclError::refactor(err.msg))?;
6702 find.found.into_inner().ok_or_else(|| KclError::refactor(
6703 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?"),
6704 ))
6705 }
6706}
6707
6708fn format_kcl_error_message(prefix: &str, error: &KclError) -> String {
6709 let message = error.message().trim();
6710 let message = if message.is_empty() {
6711 "unknown parse error"
6712 } else {
6713 message
6714 };
6715
6716 format!("{prefix}: {message}")
6717}
6718
6719fn parse_frontend_mutation_source(source: &str, parse_error_prefix: &str, no_ast_message: &str) -> ExecResult<Program> {
6720 let (program, errors) = Program::parse(source).map_err(|err| {
6721 KclErrorWithOutputs::no_outputs(KclError::refactor(format_kcl_error_message(parse_error_prefix, &err)))
6722 })?;
6723 if !errors.is_empty() {
6724 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
6725 format_compilation_issues(parse_error_prefix, &errors),
6726 )));
6727 }
6728
6729 program.ok_or_else(|| KclErrorWithOutputs::no_outputs(KclError::refactor(no_ast_message.to_owned())))
6730}
6731
6732fn format_compilation_issues(prefix: &str, issues: &[CompilationIssue]) -> String {
6733 let Some(first_issue) = issues
6734 .iter()
6735 .find(|issue| issue.severity.is_err())
6736 .or_else(|| issues.first())
6737 else {
6738 return prefix.to_owned();
6739 };
6740
6741 let message = first_issue.message.trim();
6742 let message = if message.is_empty() {
6743 "unknown parse error"
6744 } else {
6745 message
6746 };
6747
6748 if issues.len() > 1 {
6749 format!("{prefix}: {message} (+{} more)", issues.len() - 1)
6750 } else {
6751 format!("{prefix}: {message}")
6752 }
6753}
6754
6755fn source_from_ast(ast: &ast::Node<ast::Program>) -> String {
6756 ast.recast_top(&Default::default(), 0)
6758}
6759
6760struct FindNumericLiteral {
6761 target: SourceRange,
6762 found: Cell<Option<ast::NumericLiteral>>,
6763}
6764
6765impl<'a> crate::walk::Visitor<'a> for &FindNumericLiteral {
6766 type Error = crate::front::Error;
6767
6768 fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
6769 let Ok(node_range) = SourceRange::try_from(&node) else {
6770 return Ok(true);
6771 };
6772
6773 if node_range == self.target
6774 && let crate::walk::Node::NumericLiteral(literal) = node
6775 {
6776 self.found.set(Some(literal.inner.clone()));
6777 return Ok(false);
6778 }
6779
6780 for child in node.children().iter() {
6781 if !child.visit(*self)? {
6782 return Ok(false);
6783 }
6784 }
6785
6786 Ok(true)
6787 }
6788}
6789
6790fn numeric_literal_at_source_range(ast: &ast::Node<ast::Program>, target: SourceRange) -> Option<ast::NumericLiteral> {
6791 let find = FindNumericLiteral {
6792 target,
6793 found: Cell::new(None),
6794 };
6795 let node = crate::walk::Node::from(ast);
6796 node.visit(&find).ok()?;
6797 find.found.into_inner()
6798}
6799
6800struct FindSketchVarInitialByNodePath<'a> {
6801 target: &'a ast::NodePath,
6802 sketch_var_found: Cell<bool>,
6803 initial_literal: Cell<Option<ast::NumericLiteral>>,
6804}
6805
6806impl<'a, 'b> crate::walk::Visitor<'b> for &FindSketchVarInitialByNodePath<'a> {
6807 type Error = crate::front::Error;
6808
6809 fn visit_node(&self, node: crate::walk::Node<'b>) -> anyhow::Result<bool, Self::Error> {
6810 if let crate::walk::Node::SketchVar(sketch_var) = node
6811 && sketch_var.node_path.as_ref() == Some(self.target)
6812 {
6813 self.sketch_var_found.set(true);
6814 if let Some(initial) = &sketch_var.initial {
6815 self.initial_literal.set(Some(initial.inner.clone()));
6816 }
6817 return Ok(false);
6818 }
6819
6820 for child in node.children().iter() {
6821 if !child.visit(*self)? {
6822 return Ok(false);
6823 }
6824 }
6825
6826 Ok(true)
6827 }
6828}
6829
6830fn numeric_literal_at_node_path(
6840 ast: &ast::Node<ast::Program>,
6841 node_path: Option<&ast::NodePath>,
6842 source_range: SourceRange,
6843) -> Option<Option<ast::NumericLiteral>> {
6844 let Some(node_path) = node_path else {
6845 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";
6846 #[cfg(target_arch = "wasm32")]
6847 web_sys::console::warn_1(&message.into());
6848 #[cfg(not(target_arch = "wasm32"))]
6849 eprintln!("WARNING: {message}");
6850 return numeric_literal_at_source_range(ast, source_range).map(Some);
6851 };
6852 let find = FindSketchVarInitialByNodePath {
6853 target: node_path,
6854 sketch_var_found: Cell::new(false),
6855 initial_literal: Cell::new(None),
6856 };
6857 let node = crate::walk::Node::from(ast);
6858 node.visit(&find).ok()?;
6859 if !find.sketch_var_found.get() {
6860 return None;
6861 }
6862 Some(find.initial_literal.into_inner())
6863}
6864
6865fn suffix_length_unit(suffix: NumericSuffix) -> Option<UnitLength> {
6866 match suffix {
6867 NumericSuffix::Mm => Some(UnitLength::Millimeters),
6868 NumericSuffix::Cm => Some(UnitLength::Centimeters),
6869 NumericSuffix::M => Some(UnitLength::Meters),
6870 NumericSuffix::Inch => Some(UnitLength::Inches),
6871 NumericSuffix::Ft => Some(UnitLength::Feet),
6872 NumericSuffix::Yd => Some(UnitLength::Yards),
6873 _ => None,
6874 }
6875}
6876
6877fn number_value_in_default_length_units(number: Number, default_length_unit: UnitLength) -> f64 {
6878 match suffix_length_unit(number.units) {
6879 Some(unit) => adjust_length(unit, number.value, default_length_unit).0,
6880 None => number.value,
6881 }
6882}
6883
6884fn literal_value_in_default_length_units(literal: &ast::NumericLiteral, default_length_unit: UnitLength) -> f64 {
6885 match suffix_length_unit(literal.suffix) {
6886 Some(unit) => adjust_length(unit, literal.value, default_length_unit).0,
6887 None => literal.value,
6888 }
6889}
6890
6891fn var_solution_needs_commit(
6892 current_literal: &ast::NumericLiteral,
6893 solved_value: Number,
6894 default_length_unit: UnitLength,
6895) -> bool {
6896 let current = literal_value_in_default_length_units(current_literal, default_length_unit);
6897 let solved = number_value_in_default_length_units(solved_value, default_length_unit);
6898
6899 (current - solved).abs() > 1e-9
6900}
6901
6902fn preserve_var_solution_literal_style(
6903 current_literal: &ast::NumericLiteral,
6904 solved_value: Number,
6905 default_length_unit: UnitLength,
6906) -> Number {
6907 if current_literal.suffix == NumericSuffix::None {
6908 return Number {
6909 value: number_value_in_default_length_units(solved_value, default_length_unit),
6910 units: NumericSuffix::None,
6911 };
6912 }
6913
6914 let Some(current_unit) = suffix_length_unit(current_literal.suffix) else {
6915 return solved_value;
6916 };
6917
6918 let solved_default_value = number_value_in_default_length_units(solved_value, default_length_unit);
6919 Number {
6920 value: adjust_length(default_length_unit, solved_default_value, current_unit).0,
6921 units: current_literal.suffix,
6922 }
6923}
6924
6925pub(crate) fn to_ast_point2d(point: &Point2d<Expr>) -> anyhow::Result<ast::Expr> {
6926 Ok(ast::Expr::ArrayExpression(BoxNode::new(ast::Node {
6927 inner: ast::ArrayExpression {
6928 elements: vec![to_source_expr(&point.x)?, to_source_expr(&point.y)?],
6929 non_code_meta: Default::default(),
6930 digest: None,
6931 },
6932 start: Default::default(),
6933 end: Default::default(),
6934 module_id: Default::default(),
6935 node_path: None,
6936 outer_attrs: Default::default(),
6937 pre_comments: Default::default(),
6938 comment_start: Default::default(),
6939 })))
6940}
6941
6942pub(crate) fn to_ast_point2d_array(points: &[Point2d<Expr>]) -> anyhow::Result<ast::Expr> {
6943 Ok(ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(
6944 ast::ArrayExpression {
6945 elements: points.iter().map(to_ast_point2d).collect::<anyhow::Result<Vec<_>>>()?,
6946 digest: None,
6947 non_code_meta: Default::default(),
6948 },
6949 ))))
6950}
6951
6952fn to_ast_point2d_number(point: &Point2d<Number>) -> anyhow::Result<ast::Expr> {
6953 Ok(ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(
6954 ast::ArrayExpression {
6955 elements: vec![
6956 ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal::from(to_source_number(
6957 point.x,
6958 )?)))),
6959 ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal::from(to_source_number(
6960 point.y,
6961 )?)))),
6962 ],
6963 non_code_meta: Default::default(),
6964 digest: None,
6965 },
6966 ))))
6967}
6968
6969fn to_source_expr(expr: &Expr) -> anyhow::Result<ast::Expr> {
6970 match expr {
6971 Expr::Number(number) => Ok(ast::Expr::Literal(BoxNode::new(ast::Node {
6972 inner: ast::Literal::from(to_source_number(*number)?),
6973 start: Default::default(),
6974 end: Default::default(),
6975 module_id: Default::default(),
6976 node_path: None,
6977 outer_attrs: Default::default(),
6978 pre_comments: Default::default(),
6979 comment_start: Default::default(),
6980 }))),
6981 Expr::Var(number) => Ok(ast::Expr::SketchVar(BoxNode::new(ast::Node {
6982 inner: ast::SketchVar {
6983 initial: Some(BoxNode::new(ast::Node {
6984 inner: to_source_number(*number)?,
6985 start: Default::default(),
6986 end: Default::default(),
6987 module_id: Default::default(),
6988 node_path: None,
6989 outer_attrs: Default::default(),
6990 pre_comments: Default::default(),
6991 comment_start: Default::default(),
6992 })),
6993 digest: None,
6994 },
6995 start: Default::default(),
6996 end: Default::default(),
6997 module_id: Default::default(),
6998 node_path: None,
6999 outer_attrs: Default::default(),
7000 pre_comments: Default::default(),
7001 comment_start: Default::default(),
7002 }))),
7003 Expr::Variable(variable) => Ok(ast_name_expr(variable.clone())),
7004 }
7005}
7006
7007fn to_source_number(number: Number) -> anyhow::Result<ast::NumericLiteral> {
7008 Ok(ast::NumericLiteral {
7009 value: number.value,
7010 suffix: number.units,
7011 raw: format_number_literal(number.value, number.units, None)?,
7012 digest: None,
7013 })
7014}
7015
7016pub(crate) fn ast_name_expr(name: String) -> ast::Expr {
7017 ast::Expr::Name(BoxNode::new(ast_name(name)))
7018}
7019
7020fn ast_name(name: String) -> ast::Node<ast::Name> {
7021 ast::Node {
7022 inner: ast::Name {
7023 name: ast::Node {
7024 inner: ast::Identifier { name, digest: None },
7025 start: Default::default(),
7026 end: Default::default(),
7027 module_id: Default::default(),
7028 node_path: None,
7029 outer_attrs: Default::default(),
7030 pre_comments: Default::default(),
7031 comment_start: Default::default(),
7032 },
7033 path: Vec::new(),
7034 abs_path: false,
7035 digest: None,
7036 },
7037 start: Default::default(),
7038 end: Default::default(),
7039 module_id: Default::default(),
7040 node_path: None,
7041 outer_attrs: Default::default(),
7042 pre_comments: Default::default(),
7043 comment_start: Default::default(),
7044 }
7045}
7046
7047pub(crate) fn ast_sketch2_name(name: &str) -> ast::Name {
7048 ast::Name {
7049 name: ast::Node {
7050 inner: ast::Identifier {
7051 name: name.to_owned(),
7052 digest: None,
7053 },
7054 start: Default::default(),
7055 end: Default::default(),
7056 module_id: Default::default(),
7057 node_path: None,
7058 outer_attrs: Default::default(),
7059 pre_comments: Default::default(),
7060 comment_start: Default::default(),
7061 },
7062 path: Default::default(),
7063 abs_path: false,
7064 digest: None,
7065 }
7066}
7067
7068pub(crate) fn create_coincident_ast(exprs: impl IntoIterator<Item = ast::Expr>) -> ast::Expr {
7070 let elements = exprs.into_iter().collect::<Vec<_>>();
7071 debug_assert!(elements.len() >= 2, "Coincident AST should have at least 2 inputs");
7072
7073 let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7075 elements,
7076 digest: None,
7077 non_code_meta: Default::default(),
7078 })));
7079
7080 ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7082 callee: ast::Node::no_src(ast_sketch2_name(COINCIDENT_FN)),
7083 unlabeled: Some(array_expr),
7084 arguments: Default::default(),
7085 digest: None,
7086 non_code_meta: Default::default(),
7087 })))
7088}
7089
7090pub(crate) fn create_horizontal_ast(line_expr: ast::Expr) -> ast::Expr {
7092 ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7093 callee: ast::Node::no_src(ast_sketch2_name(HORIZONTAL_FN)),
7094 unlabeled: Some(line_expr),
7095 arguments: Default::default(),
7096 digest: None,
7097 non_code_meta: Default::default(),
7098 })))
7099}
7100
7101pub(crate) fn create_vertical_ast(line_expr: ast::Expr) -> ast::Expr {
7103 ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7104 callee: ast::Node::no_src(ast_sketch2_name(VERTICAL_FN)),
7105 unlabeled: Some(line_expr),
7106 arguments: Default::default(),
7107 digest: None,
7108 non_code_meta: Default::default(),
7109 })))
7110}
7111
7112pub(crate) fn create_member_expression(object_expr: ast::Expr, property: &str) -> ast::Expr {
7114 ast::Expr::MemberExpression(BoxNode::new(ast::Node::no_src(ast::MemberExpression {
7115 object: object_expr,
7116 property: ast::Expr::Name(BoxNode::new(ast::Node::no_src(ast::Name {
7117 name: ast::Node::no_src(ast::Identifier {
7118 name: property.to_string(),
7119 digest: None,
7120 }),
7121 path: Vec::new(),
7122 abs_path: false,
7123 digest: None,
7124 }))),
7125 computed: false,
7126 digest: None,
7127 })))
7128}
7129
7130pub(crate) fn create_index_expression(object_expr: ast::Expr, index: usize) -> ast::Expr {
7131 ast::Expr::MemberExpression(BoxNode::new(ast::Node::no_src(ast::MemberExpression {
7132 object: object_expr,
7133 property: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal::from(
7134 ast::NumericLiteral {
7135 value: index as f64,
7136 suffix: NumericSuffix::None,
7137 raw: index.to_string(),
7138 digest: None,
7139 },
7140 )))),
7141 computed: true,
7142 digest: None,
7143 })))
7144}
7145
7146fn create_fixed_point_constraint_ast(point_expr: ast::Expr, position: Point2d<Number>) -> anyhow::Result<ast::Expr> {
7148 let x_literal = ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal::from(to_source_number(
7150 position.x,
7151 )?))));
7152 let y_literal = ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal::from(to_source_number(
7153 position.y,
7154 )?))));
7155 let point_array = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7156 elements: vec![x_literal, y_literal],
7157 digest: None,
7158 non_code_meta: Default::default(),
7159 })));
7160
7161 let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7163 elements: vec![point_expr, point_array],
7164 digest: None,
7165 non_code_meta: Default::default(),
7166 })));
7167
7168 Ok(ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(
7170 ast::CallExpressionKw {
7171 callee: ast::Node::no_src(ast_sketch2_name(FIXED_FN)),
7172 unlabeled: Some(array_expr),
7173 arguments: Default::default(),
7174 digest: None,
7175 non_code_meta: Default::default(),
7176 },
7177 ))))
7178}
7179
7180pub(crate) fn create_equal_length_ast(line_exprs: Vec<ast::Expr>) -> ast::Expr {
7182 let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7183 elements: line_exprs,
7184 digest: None,
7185 non_code_meta: Default::default(),
7186 })));
7187
7188 ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7190 callee: ast::Node::no_src(ast_sketch2_name(EQUAL_LENGTH_FN)),
7191 unlabeled: Some(array_expr),
7192 arguments: Default::default(),
7193 digest: None,
7194 non_code_meta: Default::default(),
7195 })))
7196}
7197
7198pub(crate) fn create_equal_radius_ast(segment_exprs: Vec<ast::Expr>) -> ast::Expr {
7200 let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7201 elements: segment_exprs,
7202 digest: None,
7203 non_code_meta: Default::default(),
7204 })));
7205
7206 ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7207 callee: ast::Node::no_src(ast_sketch2_name(EQUAL_RADIUS_FN)),
7208 unlabeled: Some(array_expr),
7209 arguments: Default::default(),
7210 digest: None,
7211 non_code_meta: Default::default(),
7212 })))
7213}
7214
7215pub(crate) fn create_tangent_ast(seg1_expr: ast::Expr, seg2_expr: ast::Expr) -> ast::Expr {
7217 let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7218 elements: vec![seg1_expr, seg2_expr],
7219 digest: None,
7220 non_code_meta: Default::default(),
7221 })));
7222
7223 ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7224 callee: ast::Node::no_src(ast_sketch2_name(TANGENT_FN)),
7225 unlabeled: Some(array_expr),
7226 arguments: Default::default(),
7227 digest: None,
7228 non_code_meta: Default::default(),
7229 })))
7230}
7231
7232pub(crate) fn create_symmetric_ast(input_exprs: Vec<ast::Expr>, axis_expr: ast::Expr) -> ast::Expr {
7234 let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7235 elements: input_exprs,
7236 digest: None,
7237 non_code_meta: Default::default(),
7238 })));
7239 let arguments = vec![ast::LabeledArg {
7240 label: Some(ast::Identifier::new(SYMMETRIC_AXIS_PARAM)),
7241 arg: axis_expr,
7242 }];
7243
7244 ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7245 callee: ast::Node::no_src(ast_sketch2_name(SYMMETRIC_FN)),
7246 unlabeled: Some(array_expr),
7247 arguments,
7248 digest: None,
7249 non_code_meta: Default::default(),
7250 })))
7251}
7252
7253pub(crate) fn create_midpoint_ast(segment_expr: ast::Expr, point_expr: ast::Expr) -> ast::Expr {
7255 let arguments = vec![ast::LabeledArg {
7256 label: Some(ast::Identifier::new(MIDPOINT_POINT_PARAM)),
7257 arg: point_expr,
7258 }];
7259
7260 ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7261 callee: ast::Node::no_src(ast_sketch2_name(MIDPOINT_FN)),
7262 unlabeled: Some(segment_expr),
7263 arguments,
7264 digest: None,
7265 non_code_meta: Default::default(),
7266 })))
7267}
7268
7269fn issue_source_range(error: &KclError) -> SourceRange {
7274 let source_ranges = error.source_ranges();
7275 source_ranges
7276 .iter()
7277 .find(|range| range.is_top_level_module())
7278 .or_else(|| source_ranges.first())
7279 .copied()
7280 .unwrap_or_else(SourceRange::synthetic)
7281}
7282
7283#[cfg(test)]
7284mod tests {
7285 use std::sync;
7286
7287 use kcl_api::Distance;
7288 use kcl_api::Fixed;
7289 use kcl_api::FixedPoint;
7290 use kcl_api::Midpoint;
7291 use kcl_api::NumericSuffix;
7292 use kcl_api::Object;
7293 use kcl_api::Plane;
7294 use kcl_api::Sketch;
7295 use kcl_api::Tangent;
7296 use kcl_api::Vertical;
7297
7298 use super::*;
7299 use crate::engine::PlaneName;
7300 use crate::engine::engine_manager::EngineManager;
7301 use crate::execution::cache::SketchModeState;
7302
7303 fn find_first_sketch_object(scene_graph: &SceneGraph) -> Option<&Object> {
7304 for object in &scene_graph.objects {
7305 if let ObjectKind::Sketch(_) = &object.kind {
7306 return Some(object);
7307 }
7308 }
7309 None
7310 }
7311
7312 fn find_first_face_object(scene_graph: &SceneGraph) -> Option<&Object> {
7313 for object in &scene_graph.objects {
7314 if let ObjectKind::Face(_) = &object.kind {
7315 return Some(object);
7316 }
7317 }
7318 None
7319 }
7320
7321 fn find_first_wall_object_id(scene_graph: &SceneGraph) -> Option<ObjectId> {
7322 for object in &scene_graph.objects {
7323 if matches!(&object.kind, ObjectKind::Wall(_)) {
7324 return Some(object.id);
7325 }
7326 }
7327 None
7328 }
7329
7330 fn find_cap_object_id_with_solid_output_index(
7331 scene_graph: &SceneGraph,
7332 cap_kind: kcl_api::CapKind,
7333 solid_output_index: usize,
7334 ) -> Option<ObjectId> {
7335 for object in &scene_graph.objects {
7336 if matches!(&object.kind, ObjectKind::Cap(cap) if cap.kind == cap_kind && cap.solid_output_index == Some(solid_output_index))
7337 {
7338 return Some(object.id);
7339 }
7340 }
7341 None
7342 }
7343
7344 #[test]
7345 fn issue_source_range_prefers_top_level_module() {
7346 use kcl_error::ModuleId;
7347
7348 let top = SourceRange::new(10, 20, ModuleId::default());
7349 let imported = SourceRange::new(0, 5, ModuleId::from_usize(7));
7350
7351 let error = KclError::new_semantic(crate::errors::KclErrorDetails::new(
7354 "boom".to_owned(),
7355 vec![imported, top],
7356 ));
7357 assert_eq!(super::issue_source_range(&error), top);
7358
7359 let error = KclError::new_semantic(crate::errors::KclErrorDetails::new("boom".to_owned(), vec![imported]));
7361 assert_eq!(super::issue_source_range(&error), imported);
7362
7363 let error = KclError::new_semantic(crate::errors::KclErrorDetails::new("boom".to_owned(), vec![]));
7365 assert_eq!(super::issue_source_range(&error), SourceRange::synthetic());
7366 }
7367
7368 #[test]
7369 fn composite_constituent_sweeps_are_not_solid_outputs() {
7370 use kcl_api::artifact::ArtifactSweepMethod;
7371 use kcl_api::artifact::CompositeSolid;
7372 use kcl_api::artifact::CompositeSolidSubType;
7373 use kcl_api::artifact::Sweep;
7374 use kcl_api::artifact::SweepSubType;
7375
7376 let first_sweep_id = ArtifactId::new(Uuid::new_v4());
7377 let second_sweep_id = ArtifactId::new(Uuid::new_v4());
7378 let composite_id = ArtifactId::new(Uuid::new_v4());
7379 let code_ref = CodeRef::placeholder(SourceRange::synthetic());
7380 let sweep = |id| {
7381 Artifact::Sweep(Sweep {
7382 id,
7383 sub_type: SweepSubType::Extrusion,
7384 path_id: Some(ArtifactId::new(Uuid::new_v4())),
7385 surface_ids: Vec::new(),
7386 edge_ids: Vec::new(),
7387 code_ref: code_ref.clone(),
7388 source_sweep_id: None,
7389 trajectory_id: None,
7390 method: ArtifactSweepMethod::New,
7391 consumed: false,
7392 pattern_ids: Vec::new(),
7393 })
7394 };
7395 let mut artifacts = IndexMap::from([
7396 (first_sweep_id, sweep(first_sweep_id)),
7397 (second_sweep_id, sweep(second_sweep_id)),
7398 ]);
7399
7400 let top_level_graph = ArtifactGraph::from_parts(artifacts.clone(), artifacts.len());
7401 assert_eq!(
7402 solid_output_index_for_sweep(&top_level_graph, first_sweep_id, &code_ref),
7403 Some(0)
7404 );
7405 assert_eq!(
7406 solid_output_index_for_sweep(&top_level_graph, second_sweep_id, &code_ref),
7407 Some(1)
7408 );
7409
7410 artifacts.insert(
7411 composite_id,
7412 Artifact::CompositeSolid(CompositeSolid {
7413 id: composite_id,
7414 consumed: false,
7415 sub_type: CompositeSolidSubType::Union,
7416 output_index: None,
7417 solid_ids: vec![first_sweep_id, second_sweep_id],
7418 tool_ids: Vec::new(),
7419 code_ref,
7420 composite_solid_id: None,
7421 pattern_ids: Vec::new(),
7422 }),
7423 );
7424 let composite_graph = ArtifactGraph::from_parts(artifacts.clone(), artifacts.len());
7425 assert_eq!(
7426 solid_output_index_for_sweep(&composite_graph, first_sweep_id, &CodeRef::default()),
7427 None
7428 );
7429 assert_eq!(
7430 solid_output_index_for_sweep(&composite_graph, second_sweep_id, &CodeRef::default()),
7431 None
7432 );
7433 }
7434
7435 #[test]
7436 fn test_region_name_from_sweep_variable_supports_sweep_kinds() {
7437 let source = "\
7438region001 = region(point = [0.1, 0.1], sketch = s)
7439extrude001 = extrude(region001, length = 5)
7440revolve001 = revolve(region001, axis = Y)
7441sweep001 = sweep(region001, path = path001)
7442loft001 = loft(region001)
7443not_sweep001 = shell(extrude001, faces = [], thickness = 1)
7444";
7445
7446 let program = Program::parse(source).unwrap().0.unwrap();
7447
7448 assert_eq!(
7449 region_name_from_sweep_variable(&program.ast, "extrude001"),
7450 Some("region001".to_owned())
7451 );
7452 assert_eq!(
7453 region_name_from_sweep_variable(&program.ast, "revolve001"),
7454 Some("region001".to_owned())
7455 );
7456 assert_eq!(
7457 region_name_from_sweep_variable(&program.ast, "sweep001"),
7458 Some("region001".to_owned())
7459 );
7460 assert_eq!(
7461 region_name_from_sweep_variable(&program.ast, "loft001"),
7462 Some("region001".to_owned())
7463 );
7464 assert_eq!(region_name_from_sweep_variable(&program.ast, "not_sweep001"), None);
7465 }
7466
7467 #[track_caller]
7468 fn expect_sketch(object: &Object) -> &Sketch {
7469 if let ObjectKind::Sketch(sketch) = &object.kind {
7470 sketch
7471 } else {
7472 panic!("Object is not a sketch: {:?}", object);
7473 }
7474 }
7475
7476 fn point_position(scene_graph: &SceneGraph, point_id: ObjectId) -> Point2d<Number> {
7477 let point_object = scene_graph.objects.get(point_id.0).unwrap();
7478 let ObjectKind::Segment {
7479 segment: Segment::Point(point),
7480 } = &point_object.kind
7481 else {
7482 panic!("Object is not a point segment: {point_object:?}");
7483 };
7484 point.position.clone()
7485 }
7486
7487 fn assert_point_position_close(actual: Point2d<Number>, expected: Point2d<Number>) {
7488 assert!((actual.x.value - expected.x.value).abs() < 1e-6);
7489 assert!((actual.y.value - expected.y.value).abs() < 1e-6);
7490 }
7491
7492 fn point_expr_mm(x: f64, y: f64) -> Point2d<Expr> {
7495 Point2d {
7496 x: Expr::Var(Number {
7497 value: x,
7498 units: NumericSuffix::Mm,
7499 }),
7500 y: Expr::Var(Number {
7501 value: y,
7502 units: NumericSuffix::Mm,
7503 }),
7504 }
7505 }
7506
7507 fn point_number_mm(x: f64, y: f64) -> Point2d<Number> {
7510 Point2d {
7511 x: Number {
7512 value: x,
7513 units: NumericSuffix::Mm,
7514 },
7515 y: Number {
7516 value: y,
7517 units: NumericSuffix::Mm,
7518 },
7519 }
7520 }
7521
7522 fn make_line_ctor(start_x: f64, start_y: f64, end_x: f64, end_y: f64, units: NumericSuffix) -> LineCtor {
7523 LineCtor {
7524 start: Point2d {
7525 x: Expr::Number(Number { value: start_x, units }),
7526 y: Expr::Number(Number { value: start_y, units }),
7527 },
7528 end: Point2d {
7529 x: Expr::Number(Number { value: end_x, units }),
7530 y: Expr::Number(Number { value: end_y, units }),
7531 },
7532 construction: None,
7533 }
7534 }
7535
7536 async fn create_sketch_with_single_line(
7537 frontend: &mut FrontendState,
7538 ctx: &ExecutorContext,
7539 mock_ctx: &ExecutorContext,
7540 version: Version,
7541 ) -> (ObjectId, ObjectId, SourceDelta, SceneGraphDelta) {
7542 frontend.program = Program::empty();
7543
7544 let sketch_args = SketchCtor {
7545 on: Plane::Default(PlaneName::Xy),
7546 };
7547 let (_src_delta, _scene_delta, sketch_id) = frontend
7548 .new_sketch(ctx, ProjectId(0), FileId(0), version, sketch_args)
7549 .await
7550 .unwrap();
7551
7552 let segment = SegmentCtor::Line(make_line_ctor(0.0, 0.0, 10.0, 10.0, NumericSuffix::Mm));
7553 let (source_delta, scene_graph_delta) = frontend
7554 .add_segment(mock_ctx, version, sketch_id, segment, None)
7555 .await
7556 .unwrap();
7557 let line_id = *scene_graph_delta
7558 .new_objects
7559 .last()
7560 .expect("Expected line object id to be created");
7561
7562 (sketch_id, line_id, source_delta, scene_graph_delta)
7563 }
7564
7565 async fn seed_frontend_with_mock(frontend: &mut FrontendState, mock_ctx: &ExecutorContext, program: &Program) {
7566 frontend.program = program.clone();
7567 let outcome = mock_ctx.run_mock(program, &MockConfig::default()).await.unwrap();
7568 frontend.update_state_after_exec(outcome, true);
7569 }
7570
7571 #[test]
7572 fn test_parse_frontend_mutation_source_error_messages_are_user_facing() {
7573 for (source, expected_message) in [
7574 ("**", "Error parsing KCL source after editing: Unexpected token: *"),
7575 (
7576 "3'",
7577 "Error parsing KCL source after editing: unterminated string literal",
7578 ),
7579 ] {
7580 let err = parse_frontend_mutation_source(
7581 source,
7582 "Error parsing KCL source after editing",
7583 "No AST produced after editing",
7584 )
7585 .expect_err("expected invalid KCL source to fail");
7586 let message = err.error.message();
7587
7588 assert_eq!(message, expected_message);
7589 assert!(!message.contains("CompilationIssue"));
7590 assert!(!message.contains("KclErrorDetails"));
7591 assert!(!message.contains("source_range"));
7592 }
7593 }
7594
7595 #[tokio::test(flavor = "multi_thread")]
7596 async fn test_edit_constraint_value_parse_error_messages_are_user_facing() {
7597 let initial_source = "\
7598sketch(on = XY) {
7599 line1 = line(start = [var 0, var 0], end = [var 10, var 0])
7600 distance([line1.start, line1.end]) == 10
7601}
7602";
7603 let program = Program::parse(initial_source).unwrap().0.unwrap();
7604
7605 let mut frontend = FrontendState::new();
7606 let mock_ctx = ExecutorContext::new_mock(None).await;
7607 let version = Version(0);
7608
7609 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
7610 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7611 let sketch_id = sketch_object.id;
7612 let sketch = expect_sketch(sketch_object);
7613 let constraint_id = *sketch.constraints.first().expect("expected distance constraint");
7614
7615 for (value, expected_message) in [
7616 ("**", "Invalid constraint value: Unexpected token: *"),
7617 ("3'", "Invalid constraint value: unterminated string literal"),
7618 ] {
7619 let err = frontend
7620 .edit_constraint_value(&mock_ctx, version, sketch_id, constraint_id, value.to_owned())
7621 .await
7622 .expect_err("expected invalid constraint expression to fail");
7623 let message = err.error.message();
7624
7625 assert_eq!(message, expected_message);
7626 assert!(!message.contains("CompilationIssue"));
7627 assert!(!message.contains("KclErrorDetails"));
7628 assert!(!message.contains("source_range"));
7629 }
7630
7631 mock_ctx.close().await;
7632 }
7633
7634 #[tokio::test(flavor = "multi_thread")]
7635 async fn test_failed_edit_constraint_value_does_not_update_program() {
7636 let initial_source = "\
7637sketch(on = XY) {
7638 line1 = line(start = [var 0, var 0], end = [var 10, var 0])
7639 distance([line1.start, line1.end]) == 10
7640}
7641";
7642 let program = Program::parse(initial_source).unwrap().0.unwrap();
7643 let original_source = program.original_file_contents.clone();
7644
7645 let mut frontend = FrontendState::new();
7646 let mock_ctx = ExecutorContext::new_mock(None).await;
7647 let version = Version(0);
7648
7649 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
7650 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7651 let sketch_id = sketch_object.id;
7652 let sketch = expect_sketch(sketch_object);
7653 let constraint_id = *sketch.constraints.first().expect("expected distance constraint");
7654
7655 frontend
7656 .edit_constraint_value(
7657 &mock_ctx,
7658 version,
7659 sketch_id,
7660 constraint_id,
7661 "unknownDistance".to_owned(),
7662 )
7663 .await
7664 .expect_err("expected invalid constraint value to fail execution");
7665
7666 assert_eq!(frontend.program.original_file_contents, original_source);
7667 assert!(!frontend.program.original_file_contents.contains("unknownDistance"));
7668
7669 mock_ctx.close().await;
7670 }
7671
7672 #[tokio::test(flavor = "multi_thread")]
7673 async fn test_edit_constraint_value_array_index_oob_fails_in_sketch_mode() {
7674 let initial_source = "\
7675arr = [0]
7676sketch(on = XY) {
7677 line1 = line(start = [var 0, var 0], end = [var 10, var 0])
7678 distance([line1.start, line1.end]) == 10
7679}
7680";
7681 let program = Program::parse(initial_source).unwrap().0.unwrap();
7682
7683 let mut frontend = FrontendState::new();
7684 let mock_ctx = ExecutorContext::new_mock(None).await;
7685 let version = Version(0);
7686
7687 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
7688 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7689 let sketch_id = sketch_object.id;
7690 let sketch = expect_sketch(sketch_object);
7691 let constraint_id = *sketch.constraints.first().expect("expected distance constraint");
7692
7693 let err = frontend
7697 .edit_constraint_value(&mock_ctx, version, sketch_id, constraint_id, "arr[5]".to_owned())
7698 .await
7699 .expect_err("expected out-of-bounds array index to be an error in sketch mode execution");
7700 let message = err.error.message();
7701 assert!(
7702 message.contains("The array doesn't have any item at index 5"),
7703 "unexpected error message: {message}"
7704 );
7705
7706 mock_ctx.close().await;
7707 }
7708
7709 #[tokio::test(flavor = "multi_thread")]
7710 async fn test_sketch_checkpoint_round_trip_restores_state() {
7711 let mut frontend = FrontendState::new();
7712 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
7713 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
7714 let version = Version(0);
7715
7716 let (sketch_id, line_id, source_delta, scene_graph_delta) =
7717 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7718
7719 let expected_source = source_delta.text.clone();
7720 let expected_scene_graph = frontend.scene_graph.clone();
7721 let expected_exec_outcome = scene_graph_delta.exec_outcome.clone();
7722 let expected_point_freedom_cache = frontend.point_freedom_cache.clone();
7723
7724 let checkpoint_id = frontend
7725 .create_sketch_checkpoint(&ctx.cache, scene_graph_delta.exec_outcome.clone())
7726 .await
7727 .unwrap();
7728
7729 let edited_segments = vec![ExistingSegmentCtor {
7730 id: line_id,
7731 ctor: SegmentCtor::Line(make_line_ctor(1.0, 2.0, 13.0, 14.0, NumericSuffix::Mm)),
7732 }];
7733 let (edited_source, _edited_scene) = frontend
7734 .edit_segments(&mock_ctx, version, sketch_id, edited_segments)
7735 .await
7736 .unwrap();
7737 assert_ne!(edited_source.text, expected_source);
7738
7739 let restored = frontend
7740 .restore_sketch_checkpoint(&ctx.cache, checkpoint_id)
7741 .await
7742 .unwrap();
7743
7744 assert_eq!(restored.source_delta.text, expected_source);
7745 assert_eq!(restored.scene_graph_delta.new_graph, expected_scene_graph);
7746 assert!(restored.scene_graph_delta.invalidates_ids);
7747 assert_eq!(restored.scene_graph_delta.exec_outcome, expected_exec_outcome);
7748 assert_eq!(frontend.scene_graph, expected_scene_graph);
7749 assert_eq!(frontend.point_freedom_cache, expected_point_freedom_cache);
7750
7751 ctx.close().await;
7752 }
7753
7754 #[tokio::test(flavor = "multi_thread")]
7755 async fn test_sketch_checkpoints_prune_oldest_entries() {
7756 let mut frontend = FrontendState::new();
7757 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
7758 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
7759 let version = Version(0);
7760
7761 let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
7762 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7763
7764 let mut checkpoint_ids = Vec::new();
7765 for _ in 0..(MAX_SKETCH_CHECKPOINTS + 3) {
7766 checkpoint_ids.push(
7767 frontend
7768 .create_sketch_checkpoint(&ctx.cache, scene_graph_delta.exec_outcome.clone())
7769 .await
7770 .unwrap(),
7771 );
7772 }
7773
7774 assert_eq!(frontend.sketch_checkpoints.len(), MAX_SKETCH_CHECKPOINTS);
7775 assert!(checkpoint_ids.windows(2).all(|ids| ids[0] < ids[1]));
7776
7777 let oldest_retained = checkpoint_ids[3];
7778 assert_eq!(
7779 frontend.sketch_checkpoints.front().map(|checkpoint| checkpoint.id),
7780 Some(oldest_retained)
7781 );
7782
7783 let evicted_restore = frontend.restore_sketch_checkpoint(&ctx.cache, checkpoint_ids[0]).await;
7784 assert!(evicted_restore.is_err());
7785 assert!(evicted_restore.unwrap_err().msg.contains("Sketch checkpoint not found"));
7786
7787 frontend
7788 .restore_sketch_checkpoint(&ctx.cache, *checkpoint_ids.last().unwrap())
7789 .await
7790 .unwrap();
7791
7792 ctx.close().await;
7793 }
7794
7795 #[tokio::test(flavor = "multi_thread")]
7796 async fn test_sketch_checkpoint_uses_explicit_session_cache() {
7797 let mut frontend = FrontendState::new();
7798 let ctx = ExecutorContext::new_with_engine(sync::Arc::new(EngineManager::new_mock()), Default::default());
7799 let other = ExecutorContext::new_mock(None).await;
7800 let program = Program::parse_no_errs("@settings(kclVersion = 3.0)\nx = 2").unwrap();
7801 let outcome = ctx.run_with_caching(program).await.unwrap();
7802 let checkpoint = frontend.create_sketch_checkpoint(&ctx.cache, outcome).await.unwrap();
7803 ctx.cache.clear_mem_cache().await;
7804 assert!(ctx.cache.read_old_memory_var("x").await.is_none());
7805 frontend
7806 .restore_sketch_checkpoint(&ctx.cache, checkpoint)
7807 .await
7808 .unwrap();
7809 assert!(ctx.cache.read_old_memory_var("x").await.is_some());
7810 assert!(other.cache.read_old_memory_var("x").await.is_none());
7811 ctx.close().await;
7812 other.close().await;
7813 }
7814
7815 #[tokio::test(flavor = "multi_thread")]
7816 async fn test_restore_sketch_checkpoint_missing_id_returns_error() {
7817 let mut frontend = FrontendState::new();
7818 let ctx = ExecutorContext::new_mock(None).await;
7819 let missing_checkpoint = SketchCheckpointId::new(999);
7820
7821 let err = frontend
7822 .restore_sketch_checkpoint(&ctx.cache, missing_checkpoint)
7823 .await
7824 .expect_err("Expected restore to fail for missing checkpoint");
7825
7826 assert!(err.msg.contains("Sketch checkpoint not found"));
7827 }
7828
7829 #[tokio::test(flavor = "multi_thread")]
7830 async fn test_clear_sketch_checkpoints_removes_all_restore_points() {
7831 let mut frontend = FrontendState::new();
7832 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
7833 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
7834 let version = Version(0);
7835
7836 let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
7837 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7838
7839 let checkpoint_a = frontend
7840 .create_sketch_checkpoint(&ctx.cache, scene_graph_delta.exec_outcome.clone())
7841 .await
7842 .unwrap();
7843 let checkpoint_b = frontend
7844 .create_sketch_checkpoint(&ctx.cache, scene_graph_delta.exec_outcome.clone())
7845 .await
7846 .unwrap();
7847 assert_eq!(frontend.sketch_checkpoints.len(), 2);
7848
7849 frontend.clear_sketch_checkpoints();
7850 assert!(frontend.sketch_checkpoints.is_empty());
7851 frontend
7852 .restore_sketch_checkpoint(&ctx.cache, checkpoint_a)
7853 .await
7854 .unwrap_err();
7855 frontend
7856 .restore_sketch_checkpoint(&ctx.cache, checkpoint_b)
7857 .await
7858 .unwrap_err();
7859
7860 ctx.close().await;
7861 }
7862
7863 #[tokio::test(flavor = "multi_thread")]
7864 async fn test_hack_set_program_keeps_old_checkpoints_and_adds_fresh_baseline() {
7865 let mut frontend = FrontendState::new();
7866 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
7867 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
7868 let version = Version(0);
7869
7870 let (_sketch_id, _line_id, source_delta, scene_graph_delta) =
7871 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7872 let old_source = source_delta.text.clone();
7873 let old_checkpoint = frontend
7874 .create_sketch_checkpoint(&ctx.cache, scene_graph_delta.exec_outcome.clone())
7875 .await
7876 .unwrap();
7877 let initial_checkpoint_count = frontend.sketch_checkpoints.len();
7878
7879 let new_program = Program::parse("sketch(on = XY) {\n point(at = [1mm, 2mm])\n}\n")
7880 .unwrap()
7881 .0
7882 .unwrap();
7883
7884 let result = frontend.hack_set_program(&ctx, new_program).await.unwrap();
7885 let SetProgramOutcome::Success {
7886 checkpoint_id: Some(new_checkpoint),
7887 ..
7888 } = result
7889 else {
7890 panic!("Expected Success with a fresh checkpoint baseline");
7891 };
7892
7893 assert_eq!(frontend.sketch_checkpoints.len(), initial_checkpoint_count + 1);
7894
7895 let old_restore = frontend
7896 .restore_sketch_checkpoint(&ctx.cache, old_checkpoint)
7897 .await
7898 .unwrap();
7899 assert_eq!(old_restore.source_delta.text, old_source);
7900
7901 let new_restore = frontend
7902 .restore_sketch_checkpoint(&ctx.cache, new_checkpoint)
7903 .await
7904 .unwrap();
7905 assert!(new_restore.source_delta.text.contains("point(at = [1mm, 2mm])"));
7906
7907 ctx.close().await;
7908 }
7909
7910 #[tokio::test(flavor = "multi_thread")]
7911 async fn test_hack_set_program_exec_failure_does_not_add_checkpoint() {
7912 let mut frontend = FrontendState::new();
7913 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
7914 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
7915 let version = Version(0);
7916
7917 let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
7918 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7919 let old_checkpoint = frontend
7920 .create_sketch_checkpoint(&ctx.cache, scene_graph_delta.exec_outcome.clone())
7921 .await
7922 .unwrap();
7923 let checkpoint_count_before = frontend.sketch_checkpoints.len();
7924
7925 let failing_program = Program::parse(
7926 "sketch(on = XY) {\n line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])\n}\n\nbad = missing_name\n",
7927 )
7928 .unwrap()
7929 .0
7930 .unwrap();
7931
7932 let result = frontend.hack_set_program(&ctx, failing_program).await.unwrap();
7933 assert!(matches!(result, SetProgramOutcome::ExecFailure { .. }));
7934 assert_eq!(frontend.sketch_checkpoints.len(), checkpoint_count_before);
7935 frontend
7936 .restore_sketch_checkpoint(&ctx.cache, old_checkpoint)
7937 .await
7938 .unwrap();
7939
7940 ctx.close().await;
7941 }
7942
7943 #[tokio::test(flavor = "multi_thread")]
7944 async fn test_restore_sketch_checkpoint_restores_and_clears_mock_memory() {
7945 let mut frontend = FrontendState::new();
7946 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
7947
7948 let program = Program::parse(
7949 "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",
7950 )
7951 .unwrap()
7952 .0
7953 .unwrap();
7954 let set_program_outcome = frontend.hack_set_program(&ctx, program).await.unwrap();
7955 let SetProgramOutcome::Success { exec_outcome, .. } = set_program_outcome else {
7956 panic!("Expected successful baseline program execution");
7957 };
7958
7959 ctx.cache.clear_mem_cache().await;
7960 assert!(ctx.cache.read_old_memory().await.is_none());
7961
7962 let checkpoint_without_mock_memory = frontend
7963 .create_sketch_checkpoint(&ctx.cache, (*exec_outcome).clone())
7964 .await
7965 .unwrap();
7966
7967 ctx.cache.write_old_memory(SketchModeState::new_for_tests()).await;
7968 assert!(ctx.cache.read_old_memory().await.is_some());
7969
7970 let checkpoint_with_mock_memory = frontend
7971 .create_sketch_checkpoint(&ctx.cache, (*exec_outcome).clone())
7972 .await
7973 .unwrap();
7974
7975 ctx.cache.clear_mem_cache().await;
7976 assert!(ctx.cache.read_old_memory().await.is_none());
7977
7978 frontend
7979 .restore_sketch_checkpoint(&ctx.cache, checkpoint_with_mock_memory)
7980 .await
7981 .unwrap();
7982 assert!(ctx.cache.read_old_memory().await.is_some());
7983
7984 frontend
7985 .restore_sketch_checkpoint(&ctx.cache, checkpoint_without_mock_memory)
7986 .await
7987 .unwrap();
7988 assert!(ctx.cache.read_old_memory().await.is_none());
7989
7990 ctx.close().await;
7991 }
7992
7993 #[tokio::test(flavor = "multi_thread")]
7994 async fn test_hack_set_program_exec_error_still_allows_edit_sketch() {
7995 let source = "\
7996sketch(on = XY) {
7997 line1 = line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])
7998}
7999
8000bad = missing_name
8001";
8002 let program = Program::parse(source).unwrap().0.unwrap();
8003
8004 let mut frontend = FrontendState::new();
8005
8006 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
8007 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
8008 let version = Version(0);
8009 let project_id = ProjectId(0);
8010 let file_id = FileId(0);
8011
8012 let SetProgramOutcome::ExecFailure { .. } = frontend.hack_set_program(&ctx, program).await.unwrap() else {
8013 panic!("Expected ExecFailure from hack_set_program due to syntax error in program");
8014 };
8015
8016 let sketch_id = frontend
8017 .scene_graph
8018 .objects
8019 .iter()
8020 .find_map(|obj| matches!(obj.kind, ObjectKind::Sketch(_)).then_some(obj.id))
8021 .expect("Expected sketch object from errored hack_set_program");
8022
8023 frontend
8024 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
8025 .await
8026 .unwrap();
8027
8028 ctx.close().await;
8029 mock_ctx.close().await;
8030 }
8031
8032 #[tokio::test(flavor = "multi_thread")]
8033 async fn test_new_sketch_add_point_edit_point() {
8034 let program = Program::empty();
8035
8036 let mut frontend = FrontendState::new();
8037 frontend.program = program;
8038
8039 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
8040 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
8041 let version = Version(0);
8042
8043 let sketch_args = SketchCtor {
8044 on: Plane::Default(PlaneName::Xy),
8045 };
8046 let (_src_delta, scene_delta, sketch_id) = frontend
8047 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
8048 .await
8049 .unwrap();
8050 assert_eq!(sketch_id, ObjectId(1));
8051 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
8052 let sketch_object = &scene_delta.new_graph.objects[1];
8053 assert_eq!(sketch_object.id, ObjectId(1));
8054 assert_eq!(
8055 sketch_object.kind,
8056 ObjectKind::Sketch(Sketch {
8057 args: SketchCtor {
8058 on: Plane::Default(PlaneName::Xy)
8059 },
8060 plane: ObjectId(0),
8061 segments: vec![],
8062 constraints: vec![],
8063 })
8064 );
8065 assert_eq!(scene_delta.new_graph.objects.len(), 2);
8066
8067 let point_ctor = PointCtor {
8068 position: Point2d {
8069 x: Expr::Number(Number {
8070 value: 1.0,
8071 units: NumericSuffix::Inch,
8072 }),
8073 y: Expr::Number(Number {
8074 value: 2.0,
8075 units: NumericSuffix::Inch,
8076 }),
8077 },
8078 };
8079 let segment = SegmentCtor::Point(point_ctor);
8080 let (src_delta, scene_delta) = frontend
8081 .add_segment(&mock_ctx, version, sketch_id, segment, None)
8082 .await
8083 .unwrap();
8084 insta::assert_snapshot!("test_new_sketch_add_point_edit_point_1", src_delta.text.as_str());
8085 assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
8086 assert_eq!(scene_delta.new_graph.objects.len(), 3);
8087 for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
8088 assert_eq!(scene_object.id.0, i);
8089 }
8090
8091 let point_id = *scene_delta.new_objects.last().unwrap();
8092
8093 let point_ctor = PointCtor {
8094 position: Point2d {
8095 x: Expr::Number(Number {
8096 value: 3.0,
8097 units: NumericSuffix::Inch,
8098 }),
8099 y: Expr::Number(Number {
8100 value: 4.0,
8101 units: NumericSuffix::Inch,
8102 }),
8103 },
8104 };
8105 let segments = vec![ExistingSegmentCtor {
8106 id: point_id,
8107 ctor: SegmentCtor::Point(point_ctor),
8108 }];
8109 let (src_delta, scene_delta) = frontend
8110 .edit_segments(&mock_ctx, version, sketch_id, segments)
8111 .await
8112 .unwrap();
8113 insta::assert_snapshot!("test_new_sketch_add_point_edit_point_2", src_delta.text.as_str());
8114 assert_eq!(scene_delta.new_objects, vec![]);
8115 assert_eq!(scene_delta.new_graph.objects.len(), 3);
8116
8117 ctx.close().await;
8118 mock_ctx.close().await;
8119 }
8120
8121 #[tokio::test(flavor = "multi_thread")]
8122 async fn test_new_sketch_add_line_edit_line() {
8123 let program = Program::empty();
8124
8125 let mut frontend = FrontendState::new();
8126 frontend.program = program;
8127
8128 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
8129 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
8130 let version = Version(0);
8131
8132 let sketch_args = SketchCtor {
8133 on: Plane::Default(PlaneName::Xy),
8134 };
8135 let (_src_delta, scene_delta, sketch_id) = frontend
8136 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
8137 .await
8138 .unwrap();
8139 assert_eq!(sketch_id, ObjectId(1));
8140 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
8141 let sketch_object = &scene_delta.new_graph.objects[1];
8142 assert_eq!(sketch_object.id, ObjectId(1));
8143 assert_eq!(
8144 sketch_object.kind,
8145 ObjectKind::Sketch(Sketch {
8146 args: SketchCtor {
8147 on: Plane::Default(PlaneName::Xy)
8148 },
8149 plane: ObjectId(0),
8150 segments: vec![],
8151 constraints: vec![],
8152 })
8153 );
8154 assert_eq!(scene_delta.new_graph.objects.len(), 2);
8155
8156 let line_ctor = LineCtor {
8157 start: Point2d {
8158 x: Expr::Number(Number {
8159 value: 0.0,
8160 units: NumericSuffix::Mm,
8161 }),
8162 y: Expr::Number(Number {
8163 value: 0.0,
8164 units: NumericSuffix::Mm,
8165 }),
8166 },
8167 end: Point2d {
8168 x: Expr::Number(Number {
8169 value: 10.0,
8170 units: NumericSuffix::Mm,
8171 }),
8172 y: Expr::Number(Number {
8173 value: 10.0,
8174 units: NumericSuffix::Mm,
8175 }),
8176 },
8177 construction: None,
8178 };
8179 let segment = SegmentCtor::Line(line_ctor);
8180 let (src_delta, scene_delta) = frontend
8181 .add_segment(&mock_ctx, version, sketch_id, segment, None)
8182 .await
8183 .unwrap();
8184 insta::assert_snapshot!("test_new_sketch_add_line_edit_line_1", src_delta.text.as_str());
8185 assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
8186 assert_eq!(scene_delta.new_graph.objects.len(), 5);
8187 for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
8188 assert_eq!(scene_object.id.0, i);
8189 }
8190
8191 let line = *scene_delta.new_objects.last().unwrap();
8193
8194 let line_ctor = LineCtor {
8195 start: Point2d {
8196 x: Expr::Number(Number {
8197 value: 1.0,
8198 units: NumericSuffix::Mm,
8199 }),
8200 y: Expr::Number(Number {
8201 value: 2.0,
8202 units: NumericSuffix::Mm,
8203 }),
8204 },
8205 end: Point2d {
8206 x: Expr::Number(Number {
8207 value: 13.0,
8208 units: NumericSuffix::Mm,
8209 }),
8210 y: Expr::Number(Number {
8211 value: 14.0,
8212 units: NumericSuffix::Mm,
8213 }),
8214 },
8215 construction: None,
8216 };
8217 let segments = vec![ExistingSegmentCtor {
8218 id: line,
8219 ctor: SegmentCtor::Line(line_ctor),
8220 }];
8221 let (src_delta, scene_delta) = frontend
8222 .edit_segments(&mock_ctx, version, sketch_id, segments)
8223 .await
8224 .unwrap();
8225 insta::assert_snapshot!("test_new_sketch_add_line_edit_line_2", src_delta.text.as_str());
8226 assert_eq!(scene_delta.new_objects, vec![]);
8227 assert_eq!(scene_delta.new_graph.objects.len(), 5);
8228
8229 ctx.close().await;
8230 mock_ctx.close().await;
8231 }
8232
8233 #[tokio::test(flavor = "multi_thread")]
8234 async fn test_new_sketch_add_arc_edit_arc() {
8235 let program = Program::empty();
8236
8237 let mut frontend = FrontendState::new();
8238 frontend.program = program;
8239
8240 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
8241 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
8242 let version = Version(0);
8243
8244 let sketch_args = SketchCtor {
8245 on: Plane::Default(PlaneName::Xy),
8246 };
8247 let (_src_delta, scene_delta, sketch_id) = frontend
8248 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
8249 .await
8250 .unwrap();
8251 assert_eq!(sketch_id, ObjectId(1));
8252 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
8253 let sketch_object = &scene_delta.new_graph.objects[1];
8254 assert_eq!(sketch_object.id, ObjectId(1));
8255 assert_eq!(
8256 sketch_object.kind,
8257 ObjectKind::Sketch(Sketch {
8258 args: SketchCtor {
8259 on: Plane::Default(PlaneName::Xy),
8260 },
8261 plane: ObjectId(0),
8262 segments: vec![],
8263 constraints: vec![],
8264 })
8265 );
8266 assert_eq!(scene_delta.new_graph.objects.len(), 2);
8267
8268 let arc_ctor = ArcCtor {
8269 start: Point2d {
8270 x: Expr::Var(Number {
8271 value: 0.0,
8272 units: NumericSuffix::Mm,
8273 }),
8274 y: Expr::Var(Number {
8275 value: 0.0,
8276 units: NumericSuffix::Mm,
8277 }),
8278 },
8279 end: Point2d {
8280 x: Expr::Var(Number {
8281 value: 10.0,
8282 units: NumericSuffix::Mm,
8283 }),
8284 y: Expr::Var(Number {
8285 value: 10.0,
8286 units: NumericSuffix::Mm,
8287 }),
8288 },
8289 center: Point2d {
8290 x: Expr::Var(Number {
8291 value: 10.0,
8292 units: NumericSuffix::Mm,
8293 }),
8294 y: Expr::Var(Number {
8295 value: 0.0,
8296 units: NumericSuffix::Mm,
8297 }),
8298 },
8299 direction: None,
8300 construction: None,
8301 };
8302 let segment = SegmentCtor::Arc(arc_ctor);
8303 let (src_delta, scene_delta) = frontend
8304 .add_segment(&mock_ctx, version, sketch_id, segment, None)
8305 .await
8306 .unwrap();
8307 insta::assert_snapshot!("test_new_sketch_add_arc_edit_arc_1", src_delta.text.as_str());
8308 assert_eq!(
8309 scene_delta.new_objects,
8310 vec![ObjectId(2), ObjectId(3), ObjectId(4), ObjectId(5)]
8311 );
8312 for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
8313 assert_eq!(scene_object.id.0, i);
8314 }
8315 assert_eq!(scene_delta.new_graph.objects.len(), 6);
8316
8317 let arc = *scene_delta.new_objects.last().unwrap();
8319
8320 let arc_ctor = ArcCtor {
8321 start: Point2d {
8322 x: Expr::Var(Number {
8323 value: 1.0,
8324 units: NumericSuffix::Mm,
8325 }),
8326 y: Expr::Var(Number {
8327 value: 2.0,
8328 units: NumericSuffix::Mm,
8329 }),
8330 },
8331 end: Point2d {
8332 x: Expr::Var(Number {
8333 value: 13.0,
8334 units: NumericSuffix::Mm,
8335 }),
8336 y: Expr::Var(Number {
8337 value: 14.0,
8338 units: NumericSuffix::Mm,
8339 }),
8340 },
8341 center: Point2d {
8342 x: Expr::Var(Number {
8343 value: 13.0,
8344 units: NumericSuffix::Mm,
8345 }),
8346 y: Expr::Var(Number {
8347 value: 2.0,
8348 units: NumericSuffix::Mm,
8349 }),
8350 },
8351 direction: None,
8352 construction: None,
8353 };
8354 let segments = vec![ExistingSegmentCtor {
8355 id: arc,
8356 ctor: SegmentCtor::Arc(arc_ctor),
8357 }];
8358 let (src_delta, scene_delta) = frontend
8359 .edit_segments(&mock_ctx, version, sketch_id, segments)
8360 .await
8361 .unwrap();
8362 insta::assert_snapshot!("test_new_sketch_add_arc_edit_arc_2", src_delta.text.as_str());
8363 assert_eq!(scene_delta.new_objects, vec![]);
8364 assert_eq!(scene_delta.new_graph.objects.len(), 6);
8365
8366 ctx.close().await;
8367 mock_ctx.close().await;
8368 }
8369
8370 #[tokio::test(flavor = "multi_thread")]
8371 async fn test_new_sketch_add_circle_edit_circle() {
8372 let program = Program::empty();
8373
8374 let mut frontend = FrontendState::new();
8375 frontend.program = program;
8376
8377 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
8378 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
8379 let version = Version(0);
8380
8381 let sketch_args = SketchCtor {
8382 on: Plane::Default(PlaneName::Xy),
8383 };
8384 let (_src_delta, _scene_delta, sketch_id) = frontend
8385 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
8386 .await
8387 .unwrap();
8388
8389 let circle_ctor = CircleCtor {
8391 start: Point2d {
8392 x: Expr::Var(Number {
8393 value: 5.0,
8394 units: NumericSuffix::Mm,
8395 }),
8396 y: Expr::Var(Number {
8397 value: 0.0,
8398 units: NumericSuffix::Mm,
8399 }),
8400 },
8401 center: Point2d {
8402 x: Expr::Var(Number {
8403 value: 0.0,
8404 units: NumericSuffix::Mm,
8405 }),
8406 y: Expr::Var(Number {
8407 value: 0.0,
8408 units: NumericSuffix::Mm,
8409 }),
8410 },
8411 construction: None,
8412 };
8413 let segment = SegmentCtor::Circle(circle_ctor);
8414 let (src_delta, scene_delta) = frontend
8415 .add_segment(&mock_ctx, version, sketch_id, segment, None)
8416 .await
8417 .unwrap();
8418 insta::assert_snapshot!("test_new_sketch_add_circle_edit_circle_1", src_delta.text.as_str());
8419 assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
8421 assert_eq!(scene_delta.new_graph.objects.len(), 5);
8422
8423 let circle = *scene_delta.new_objects.last().unwrap();
8424
8425 let circle_ctor = CircleCtor {
8427 start: Point2d {
8428 x: Expr::Var(Number {
8429 value: 10.0,
8430 units: NumericSuffix::Mm,
8431 }),
8432 y: Expr::Var(Number {
8433 value: 0.0,
8434 units: NumericSuffix::Mm,
8435 }),
8436 },
8437 center: Point2d {
8438 x: Expr::Var(Number {
8439 value: 3.0,
8440 units: NumericSuffix::Mm,
8441 }),
8442 y: Expr::Var(Number {
8443 value: 4.0,
8444 units: NumericSuffix::Mm,
8445 }),
8446 },
8447 construction: None,
8448 };
8449 let segments = vec![ExistingSegmentCtor {
8450 id: circle,
8451 ctor: SegmentCtor::Circle(circle_ctor),
8452 }];
8453 let (src_delta, scene_delta) = frontend
8454 .edit_segments(&mock_ctx, version, sketch_id, segments)
8455 .await
8456 .unwrap();
8457 insta::assert_snapshot!("test_new_sketch_add_circle_edit_circle_2", src_delta.text.as_str());
8458 assert_eq!(scene_delta.new_objects, vec![]);
8459 assert_eq!(scene_delta.new_graph.objects.len(), 5);
8460
8461 ctx.close().await;
8462 mock_ctx.close().await;
8463 }
8464
8465 #[tokio::test(flavor = "multi_thread")]
8466 async fn test_delete_circle() {
8467 let initial_source = "sketch001 = sketch(on = XY) {
8468 circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
8469}
8470";
8471
8472 let program = Program::parse(initial_source).unwrap().0.unwrap();
8473 let mut frontend = FrontendState::new();
8474
8475 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
8476 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
8477 let version = Version(0);
8478
8479 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8480 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8481 let sketch_id = sketch_object.id;
8482 let sketch = expect_sketch(sketch_object);
8483
8484 assert_eq!(sketch.segments.len(), 3);
8486 let circle_id = sketch.segments[2];
8487
8488 let (src_delta, scene_delta) = frontend
8490 .delete_objects(&mock_ctx, version, sketch_id, vec![], vec![circle_id])
8491 .await
8492 .unwrap();
8493 insta::assert_snapshot!("test_delete_circle", src_delta.text.as_str());
8494 let new_sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8495 let new_sketch = expect_sketch(new_sketch_object);
8496 assert_eq!(new_sketch.segments.len(), 0);
8497
8498 ctx.close().await;
8499 mock_ctx.close().await;
8500 }
8501
8502 #[tokio::test(flavor = "multi_thread")]
8503 async fn test_edit_circle_via_point() {
8504 let initial_source = "sketch001 = sketch(on = XY) {
8505 circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
8506}
8507";
8508
8509 let program = Program::parse(initial_source).unwrap().0.unwrap();
8510 let mut frontend = FrontendState::new();
8511
8512 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
8513 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
8514 let version = Version(0);
8515
8516 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8517 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8518 let sketch_id = sketch_object.id;
8519 let sketch = expect_sketch(sketch_object);
8520
8521 let circle_id = sketch
8523 .segments
8524 .iter()
8525 .copied()
8526 .find(|seg_id| {
8527 matches!(
8528 &frontend.scene_graph.objects[seg_id.0].kind,
8529 ObjectKind::Segment {
8530 segment: Segment::Circle(_)
8531 }
8532 )
8533 })
8534 .expect("Expected a circle segment in sketch");
8535 let circle_object = &frontend.scene_graph.objects[circle_id.0];
8536 let ObjectKind::Segment {
8537 segment: Segment::Circle(circle),
8538 } = &circle_object.kind
8539 else {
8540 panic!("Expected circle segment, got: {:?}", circle_object.kind);
8541 };
8542 let start_point_id = circle.start;
8543
8544 let segments = vec![ExistingSegmentCtor {
8546 id: start_point_id,
8547 ctor: SegmentCtor::Point(PointCtor {
8548 position: Point2d {
8549 x: Expr::Var(Number {
8550 value: 7.0,
8551 units: NumericSuffix::Mm,
8552 }),
8553 y: Expr::Var(Number {
8554 value: 1.0,
8555 units: NumericSuffix::Mm,
8556 }),
8557 },
8558 }),
8559 }];
8560 let (src_delta, _scene_delta) = frontend
8561 .edit_segments(&mock_ctx, version, sketch_id, segments)
8562 .await
8563 .unwrap();
8564 insta::assert_snapshot!("test_edit_circle_via_point", src_delta.text.as_str());
8565
8566 ctx.close().await;
8567 mock_ctx.close().await;
8568 }
8569
8570 #[tokio::test(flavor = "multi_thread")]
8571 async fn test_add_line_when_sketch_block_uses_variable() {
8572 let initial_source = "s = sketch(on = XY) {}
8573";
8574
8575 let program = Program::parse(initial_source).unwrap().0.unwrap();
8576
8577 let mut frontend = FrontendState::new();
8578
8579 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
8580 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
8581 let version = Version(0);
8582
8583 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8584 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8585 let sketch_id = sketch_object.id;
8586
8587 let line_ctor = LineCtor {
8588 start: Point2d {
8589 x: Expr::Number(Number {
8590 value: 0.0,
8591 units: NumericSuffix::Mm,
8592 }),
8593 y: Expr::Number(Number {
8594 value: 0.0,
8595 units: NumericSuffix::Mm,
8596 }),
8597 },
8598 end: Point2d {
8599 x: Expr::Number(Number {
8600 value: 10.0,
8601 units: NumericSuffix::Mm,
8602 }),
8603 y: Expr::Number(Number {
8604 value: 10.0,
8605 units: NumericSuffix::Mm,
8606 }),
8607 },
8608 construction: None,
8609 };
8610 let segment = SegmentCtor::Line(line_ctor);
8611 let (src_delta, scene_delta) = frontend
8612 .add_segment(&mock_ctx, version, sketch_id, segment, None)
8613 .await
8614 .unwrap();
8615 insta::assert_snapshot!("test_add_line_when_sketch_block_uses_variable", src_delta.text.as_str());
8616 assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
8617 assert_eq!(scene_delta.new_graph.objects.len(), 5);
8618
8619 ctx.close().await;
8620 mock_ctx.close().await;
8621 }
8622
8623 #[tokio::test(flavor = "multi_thread")]
8624 async fn test_new_sketch_add_line_delete_sketch() {
8625 let program = Program::empty();
8626
8627 let mut frontend = FrontendState::new();
8628 frontend.program = program;
8629
8630 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
8631 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
8632 let version = Version(0);
8633
8634 let sketch_args = SketchCtor {
8635 on: Plane::Default(PlaneName::Xy),
8636 };
8637 let (_src_delta, scene_delta, sketch_id) = frontend
8638 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
8639 .await
8640 .unwrap();
8641 assert_eq!(sketch_id, ObjectId(1));
8642 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
8643 let sketch_object = &scene_delta.new_graph.objects[1];
8644 assert_eq!(sketch_object.id, ObjectId(1));
8645 assert_eq!(
8646 sketch_object.kind,
8647 ObjectKind::Sketch(Sketch {
8648 args: SketchCtor {
8649 on: Plane::Default(PlaneName::Xy)
8650 },
8651 plane: ObjectId(0),
8652 segments: vec![],
8653 constraints: vec![],
8654 })
8655 );
8656 assert_eq!(scene_delta.new_graph.objects.len(), 2);
8657
8658 let line_ctor = LineCtor {
8659 start: Point2d {
8660 x: Expr::Number(Number {
8661 value: 0.0,
8662 units: NumericSuffix::Mm,
8663 }),
8664 y: Expr::Number(Number {
8665 value: 0.0,
8666 units: NumericSuffix::Mm,
8667 }),
8668 },
8669 end: Point2d {
8670 x: Expr::Number(Number {
8671 value: 10.0,
8672 units: NumericSuffix::Mm,
8673 }),
8674 y: Expr::Number(Number {
8675 value: 10.0,
8676 units: NumericSuffix::Mm,
8677 }),
8678 },
8679 construction: None,
8680 };
8681 let segment = SegmentCtor::Line(line_ctor);
8682 let (src_delta, scene_delta) = frontend
8683 .add_segment(&mock_ctx, version, sketch_id, segment, None)
8684 .await
8685 .unwrap();
8686 insta::assert_snapshot!("test_new_sketch_add_line_delete_sketch_1", src_delta.text.as_str());
8687 assert_eq!(scene_delta.new_graph.objects.len(), 5);
8688
8689 let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8690 insta::assert_snapshot!("test_new_sketch_add_line_delete_sketch_2", src_delta.text.as_str());
8691 assert_eq!(scene_delta.new_graph.objects.len(), 0);
8692
8693 ctx.close().await;
8694 mock_ctx.close().await;
8695 }
8696
8697 #[tokio::test(flavor = "multi_thread")]
8698 async fn test_delete_sketch_when_sketch_block_uses_variable() {
8699 let initial_source = "s = sketch(on = XY) {}
8700";
8701
8702 let program = Program::parse(initial_source).unwrap().0.unwrap();
8703
8704 let mut frontend = FrontendState::new();
8705
8706 let ctx = ExecutorContext::new_with_engine(sync::Arc::new(EngineManager::new_mock()), Default::default());
8707 let version = Version(0);
8708
8709 frontend.hack_set_program(&ctx, program).await.unwrap();
8710 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8711 let sketch_id = sketch_object.id;
8712
8713 let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8714 insta::assert_snapshot!(
8715 "test_delete_sketch_when_sketch_block_uses_variable",
8716 src_delta.text.as_str()
8717 );
8718 assert_eq!(scene_delta.new_graph.objects.len(), 0);
8719
8720 ctx.close().await;
8721 }
8722
8723 #[tokio::test(flavor = "multi_thread")]
8724 async fn test_delete_sketch_after_comment() {
8725 let initial_source = "sketch001 = sketch(on = XZ) {
8726}
8727";
8728
8729 let program = Program::parse(initial_source).unwrap().0.unwrap();
8730 let mut frontend = FrontendState::new();
8731
8732 let ctx = ExecutorContext::new_with_engine(sync::Arc::new(EngineManager::new_mock()), Default::default());
8733 let version = Version(0);
8734
8735 frontend.hack_set_program(&ctx, program).await.unwrap();
8736 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8737 let sketch_id = sketch_object.id;
8738 let original_source = sketch_object.source.clone();
8739
8740 let commented_source = "// test 1
8741sketch001 = sketch(on = XZ) {
8742}
8743";
8744 let commented_program = Program::parse(commented_source).unwrap().0.unwrap();
8745 frontend.engine_execute(&ctx, commented_program).await.unwrap();
8746
8747 let cached_sketch_object = &frontend.scene_graph.objects[sketch_id.0];
8748 assert_eq!(cached_sketch_object.source, original_source);
8749
8750 let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8751 assert!(
8752 !src_delta.text.contains("sketch001"),
8753 "sketch was not deleted: {}",
8754 src_delta.text
8755 );
8756 insta::assert_snapshot!("test_delete_sketch_after_comment", src_delta.text.as_str());
8758 assert_eq!(scene_delta.new_graph.objects.len(), 0);
8759
8760 ctx.close().await;
8761 }
8762
8763 #[tokio::test(flavor = "multi_thread")]
8764 async fn test_delete_sketch_preserves_pre_comment_when_followed_by_code() {
8765 let initial_source = "sketch001 = sketch(on = XZ) {
8766}
8767foo = 1
8768";
8769
8770 let program = Program::parse(initial_source).unwrap().0.unwrap();
8771 let mut frontend = FrontendState::new();
8772
8773 let ctx = ExecutorContext::new_with_engine(sync::Arc::new(EngineManager::new_mock()), Default::default());
8774 let version = Version(0);
8775
8776 frontend.hack_set_program(&ctx, program).await.unwrap();
8777 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8778 let sketch_id = sketch_object.id;
8779
8780 let commented_source = "// keep me
8781sketch001 = sketch(on = XZ) {
8782}
8783foo = 1
8784";
8785 let commented_program = Program::parse(commented_source).unwrap().0.unwrap();
8786 frontend.engine_execute(&ctx, commented_program).await.unwrap();
8787
8788 let (src_delta, _scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8789 insta::assert_snapshot!(
8791 "test_delete_sketch_preserves_pre_comment_when_followed_by_code",
8792 src_delta.text.as_str()
8793 );
8794
8795 ctx.close().await;
8796 }
8797
8798 #[tokio::test(flavor = "multi_thread")]
8799 async fn test_delete_segment_preserves_pre_comment() {
8800 let initial_source = "\
8801sketch(on = XY) {
8802 point(at = [var 1, var 2])
8803 // describe the middle point
8804 point(at = [var 3, var 4])
8805 point(at = [var 5, var 6])
8806}
8807";
8808
8809 let program = Program::parse(initial_source).unwrap().0.unwrap();
8810 let mut frontend = FrontendState::new();
8811
8812 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
8813 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
8814 let version = Version(0);
8815
8816 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8817 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8818 let sketch_id = sketch_object.id;
8819 let sketch = expect_sketch(sketch_object);
8820
8821 let middle_point_id = *sketch.segments.get(1).unwrap();
8822
8823 let (src_delta, _scene_delta) = frontend
8824 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![middle_point_id])
8825 .await
8826 .unwrap();
8827 insta::assert_snapshot!("test_delete_segment_preserves_pre_comment", src_delta.text.as_str());
8830
8831 ctx.close().await;
8832 mock_ctx.close().await;
8833 }
8834
8835 #[tokio::test(flavor = "multi_thread")]
8836 async fn test_delete_last_segment_preserves_pre_comment() {
8837 let initial_source = "\
8838sketch(on = XY) {
8839 point(at = [var 1, var 2])
8840 // describe the trailing point
8841 point(at = [var 3, var 4])
8842}
8843";
8844
8845 let program = Program::parse(initial_source).unwrap().0.unwrap();
8846 let mut frontend = FrontendState::new();
8847
8848 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
8849 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
8850 let version = Version(0);
8851
8852 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8853 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8854 let sketch_id = sketch_object.id;
8855 let sketch = expect_sketch(sketch_object);
8856
8857 let last_point_id = *sketch.segments.last().unwrap();
8858
8859 let (src_delta, _scene_delta) = frontend
8860 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![last_point_id])
8861 .await
8862 .unwrap();
8863 insta::assert_snapshot!(
8866 "test_delete_last_segment_preserves_pre_comment",
8867 src_delta.text.as_str()
8868 );
8869
8870 ctx.close().await;
8871 mock_ctx.close().await;
8872 }
8873
8874 #[tokio::test(flavor = "multi_thread")]
8875 async fn test_delete_segment_drops_inline_trailing_comment() {
8876 let initial_source = "\
8877sketch(on = XY) {
8878 point(at = [var 1, var 2])
8879 point(at = [var 3, var 4]) // same-line note that gets dropped
8880 point(at = [var 5, var 6])
8881}
8882";
8883
8884 let program = Program::parse(initial_source).unwrap().0.unwrap();
8885 let mut frontend = FrontendState::new();
8886
8887 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
8888 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
8889 let version = Version(0);
8890
8891 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8892 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8893 let sketch_id = sketch_object.id;
8894 let sketch = expect_sketch(sketch_object);
8895
8896 let middle_point_id = *sketch.segments.get(1).unwrap();
8897
8898 let (src_delta, _scene_delta) = frontend
8899 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![middle_point_id])
8900 .await
8901 .unwrap();
8902 assert!(
8904 !src_delta.text.contains("same-line note"),
8905 "inline comment should have been removed: {}",
8906 src_delta.text
8907 );
8908
8909 ctx.close().await;
8910 mock_ctx.close().await;
8911 }
8912
8913 #[tokio::test(flavor = "multi_thread")]
8914 async fn test_delete_segments_preserves_block_comments_across_positions() {
8915 let initial_source = "\
8923sketch(on = XY) {
8924 /* above first - moves to middle */
8925 point(at = [var 1, var 2]) /* same-line on first - dropped */
8926 /* above middle - stays */
8927 point(at = [var 3, var 4])
8928 /* above last - moves to trailing meta */
8929 point(at = [var 5, var 6])
8930}
8931";
8932
8933 let program = Program::parse(initial_source).unwrap().0.unwrap();
8934 let mut frontend = FrontendState::new();
8935
8936 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
8937 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
8938 let version = Version(0);
8939
8940 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8941 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8942 let sketch_id = sketch_object.id;
8943 let sketch = expect_sketch(sketch_object);
8944
8945 let first_point_id = *sketch.segments.first().unwrap();
8946 let last_point_id = *sketch.segments.last().unwrap();
8947
8948 let (src_delta, _scene_delta) = frontend
8949 .delete_objects(
8950 &mock_ctx,
8951 version,
8952 sketch_id,
8953 Vec::new(),
8954 vec![first_point_id, last_point_id],
8955 )
8956 .await
8957 .unwrap();
8958 insta::assert_snapshot!(
8959 "test_delete_segments_preserves_block_comments_across_positions",
8960 src_delta.text.as_str()
8961 );
8962
8963 ctx.close().await;
8964 mock_ctx.close().await;
8965 }
8966
8967 #[tokio::test(flavor = "multi_thread")]
8968 async fn test_edit_line_when_editing_its_start_point() {
8969 let initial_source = "\
8970sketch(on = XY) {
8971 line(start = [var 1, var 2], end = [var 3, var 4])
8972}
8973";
8974
8975 let program = Program::parse(initial_source).unwrap().0.unwrap();
8976
8977 let mut frontend = FrontendState::new();
8978
8979 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
8980 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
8981 let version = Version(0);
8982
8983 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8984 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8985 let sketch_id = sketch_object.id;
8986 let sketch = expect_sketch(sketch_object);
8987
8988 let point_id = *sketch.segments.first().unwrap();
8989
8990 let point_ctor = PointCtor {
8991 position: Point2d {
8992 x: Expr::Var(Number {
8993 value: 5.0,
8994 units: NumericSuffix::Inch,
8995 }),
8996 y: Expr::Var(Number {
8997 value: 6.0,
8998 units: NumericSuffix::Inch,
8999 }),
9000 },
9001 };
9002 let segments = vec![ExistingSegmentCtor {
9003 id: point_id,
9004 ctor: SegmentCtor::Point(point_ctor),
9005 }];
9006 let (src_delta, scene_delta) = frontend
9007 .edit_segments(&mock_ctx, version, sketch_id, segments)
9008 .await
9009 .unwrap();
9010 insta::assert_snapshot!("test_edit_line_when_editing_its_start_point", src_delta.text.as_str());
9011 assert_eq!(scene_delta.new_objects, vec![]);
9012 assert_eq!(scene_delta.new_graph.objects.len(), 5);
9013
9014 ctx.close().await;
9015 mock_ctx.close().await;
9016 }
9017
9018 #[tokio::test(flavor = "multi_thread")]
9019 async fn test_edit_line_when_editing_its_end_point() {
9020 let initial_source = "\
9021sketch(on = XY) {
9022 line(start = [var 1, var 2], end = [var 3, var 4])
9023}
9024";
9025
9026 let program = Program::parse(initial_source).unwrap().0.unwrap();
9027
9028 let mut frontend = FrontendState::new();
9029
9030 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
9031 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
9032 let version = Version(0);
9033
9034 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9035 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9036 let sketch_id = sketch_object.id;
9037 let sketch = expect_sketch(sketch_object);
9038 let point_id = *sketch.segments.get(1).unwrap();
9039
9040 let point_ctor = PointCtor {
9041 position: Point2d {
9042 x: Expr::Var(Number {
9043 value: 5.0,
9044 units: NumericSuffix::Inch,
9045 }),
9046 y: Expr::Var(Number {
9047 value: 6.0,
9048 units: NumericSuffix::Inch,
9049 }),
9050 },
9051 };
9052 let segments = vec![ExistingSegmentCtor {
9053 id: point_id,
9054 ctor: SegmentCtor::Point(point_ctor),
9055 }];
9056 let (src_delta, scene_delta) = frontend
9057 .edit_segments(&mock_ctx, version, sketch_id, segments)
9058 .await
9059 .unwrap();
9060 insta::assert_snapshot!("test_edit_line_when_editing_its_end_point", src_delta.text.as_str());
9061 assert_eq!(scene_delta.new_objects, vec![]);
9062 assert_eq!(
9063 scene_delta.new_graph.objects.len(),
9064 5,
9065 "{:#?}",
9066 scene_delta.new_graph.objects
9067 );
9068
9069 ctx.close().await;
9070 mock_ctx.close().await;
9071 }
9072
9073 #[tokio::test(flavor = "multi_thread")]
9074 async fn test_edit_line_with_coincident_feedback() {
9075 let initial_source = "\
9076sketch(on = XY) {
9077 line1 = line(start = [var 1, var 2], end = [var 1, var 2])
9078 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9079 fixed([line1.start, [0, 0]])
9080 coincident([line1.end, line2.start])
9081 equalLength([line1, line2])
9082}
9083";
9084
9085 let program = Program::parse(initial_source).unwrap().0.unwrap();
9086
9087 let mut frontend = FrontendState::new();
9088
9089 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
9090 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
9091 let version = Version(0);
9092
9093 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9094 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9095 let sketch_id = sketch_object.id;
9096 let sketch = expect_sketch(sketch_object);
9097 let line2_end_id = *sketch.segments.get(4).unwrap();
9098
9099 let segments = vec![ExistingSegmentCtor {
9100 id: line2_end_id,
9101 ctor: SegmentCtor::Point(PointCtor {
9102 position: Point2d {
9103 x: Expr::Var(Number {
9104 value: 9.0,
9105 units: NumericSuffix::None,
9106 }),
9107 y: Expr::Var(Number {
9108 value: 10.0,
9109 units: NumericSuffix::None,
9110 }),
9111 },
9112 }),
9113 }];
9114 let (src_delta, scene_delta) = frontend
9115 .edit_segments(&mock_ctx, version, sketch_id, segments)
9116 .await
9117 .unwrap();
9118 insta::assert_snapshot!("test_edit_line_with_coincident_feedback", src_delta.text.as_str());
9119 assert_eq!(
9120 scene_delta.new_graph.objects.len(),
9121 11,
9122 "{:#?}",
9123 scene_delta.new_graph.objects
9124 );
9125
9126 ctx.close().await;
9127 mock_ctx.close().await;
9128 }
9129
9130 #[tokio::test(flavor = "multi_thread")]
9131 async fn test_edit_segments_persists_solver_feedback_for_next_mock_execute() {
9132 let initial_source = "\
9133sketch(on = XY) {
9134 line1 = line(start = [var 1, var 2], end = [var 1, var 2])
9135 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9136 fixed([line1.start, [0, 0]])
9137 coincident([line1.end, line2.start])
9138 equalLength([line1, line2])
9139}
9140";
9141
9142 let program = Program::parse(initial_source).unwrap().0.unwrap();
9143 let mut frontend = FrontendState::new();
9144 let mock_ctx = ExecutorContext::new_mock(None).await;
9145 let version = Version(0);
9146
9147 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9148 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9149 let sketch_id = sketch_object.id;
9150 let sketch = expect_sketch(sketch_object);
9151 let line2_end_id = *sketch.segments.get(4).unwrap();
9152
9153 let segments = vec![ExistingSegmentCtor {
9154 id: line2_end_id,
9155 ctor: SegmentCtor::Point(PointCtor {
9156 position: Point2d {
9157 x: Expr::Var(Number {
9158 value: 9.0,
9159 units: NumericSuffix::None,
9160 }),
9161 y: Expr::Var(Number {
9162 value: 10.0,
9163 units: NumericSuffix::None,
9164 }),
9165 },
9166 }),
9167 }];
9168 let (edited_source, _) = frontend
9169 .edit_segments(&mock_ctx, version, sketch_id, segments)
9170 .await
9171 .unwrap();
9172
9173 let (mock_source, _) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
9174 assert_eq!(mock_source.text, edited_source.text);
9175
9176 mock_ctx.close().await;
9177 }
9178
9179 #[tokio::test(flavor = "multi_thread")]
9182 async fn test_preview_edit_segments_does_not_persist_solver_feedback() {
9183 let initial_source = "\
9184sketch(on = XY) {
9185 line1 = line(start = [var 1, var 2], end = [var 1, var 2])
9186 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9187 fixed([line1.start, [0, 0]])
9188 coincident([line1.end, line2.start])
9189 equalLength([line1, line2])
9190}
9191";
9192
9193 let program = Program::parse(initial_source).unwrap().0.unwrap();
9194 let mut frontend = FrontendState::new();
9195 let mock_ctx = ExecutorContext::new_mock(None).await;
9196 let version = Version(0);
9197
9198 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9199 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9200 let sketch_id = sketch_object.id;
9201 let sketch = expect_sketch(sketch_object);
9202 let line2_end_id = *sketch.segments.get(4).unwrap();
9203
9204 let segments = vec![ExistingSegmentCtor {
9205 id: line2_end_id,
9206 ctor: SegmentCtor::Point(PointCtor {
9207 position: Point2d {
9208 x: Expr::Var(Number {
9209 value: 9.0,
9210 units: NumericSuffix::None,
9211 }),
9212 y: Expr::Var(Number {
9213 value: 10.0,
9214 units: NumericSuffix::None,
9215 }),
9216 },
9217 }),
9218 }];
9219 let (preview_source, preview_delta) = frontend
9220 .edit_segments_with_options(
9221 &mock_ctx,
9222 version,
9223 sketch_id,
9224 segments,
9225 EditSegmentsOptions {
9226 anchor_segment_ids: Some(vec![line2_end_id]),
9227 drag_anchors: Vec::new(),
9228 constraint_label_edits: Vec::new(),
9229 commit_solved_initial_guesses: false,
9230 },
9231 )
9232 .await
9233 .unwrap();
9234
9235 assert!(
9236 !preview_delta.exec_outcome.var_solutions.is_empty(),
9237 "preview solve should still solve and return geometry feedback"
9238 );
9239 assert!(
9240 preview_source
9241 .text
9242 .contains("line1 = line(start = [var 1, var 2], end = [var 1, var 2])")
9243 );
9244 assert!(
9245 preview_source
9246 .text
9247 .contains("line2 = line(start = [var 5, var 6], end = [var 9, var 10])")
9248 );
9249
9250 let (mock_source, _) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
9251 assert_eq!(mock_source.text, preview_source.text);
9252
9253 mock_ctx.close().await;
9254 }
9255
9256 #[tokio::test(flavor = "multi_thread")]
9257 async fn test_add_constraint_persists_solver_feedback_for_next_mock_execute() {
9258 let initial_source = "\
9259sketch(on = XY) {
9260 line1 = line(start = [var 0, var 0], end = [var 10, var 0])
9261}
9262";
9263
9264 let program = Program::parse(initial_source).unwrap().0.unwrap();
9265 let mut frontend = FrontendState::new();
9266 let mock_ctx = ExecutorContext::new_mock(None).await;
9267 let version = Version(0);
9268
9269 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9270 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9271 let sketch_id = sketch_object.id;
9272 let sketch = expect_sketch(sketch_object);
9273 let line_end_id = *sketch.segments.get(1).unwrap();
9274
9275 let constraint = Constraint::Fixed(Fixed {
9276 points: vec![FixedPoint {
9277 point: line_end_id,
9278 position: Point2d {
9279 x: Number {
9280 value: 20.0,
9281 units: NumericSuffix::Mm,
9282 },
9283 y: Number {
9284 value: 0.0,
9285 units: NumericSuffix::Mm,
9286 },
9287 },
9288 }],
9289 });
9290 let (constraint_source, _) = frontend
9291 .add_constraint(&mock_ctx, version, sketch_id, constraint)
9292 .await
9293 .unwrap();
9294
9295 assert!(
9296 constraint_source
9297 .text
9298 .contains("line1 = line(start = [var 0, var 0], end = [var 20, var 0])"),
9299 "{}",
9300 constraint_source.text
9301 );
9302 let (mock_source, _) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
9303 assert_eq!(mock_source.text, constraint_source.text);
9304
9305 mock_ctx.close().await;
9306 }
9307
9308 #[test]
9309 fn test_no_solver_feedback_preserves_original_source() {
9310 let initial_source = "\
9311@settings(defaultLengthUnit = in, kclVersion = 2.0)
9312cylinder = startSketchOn(XY)
9313 |> circle(center= [0, 0], radius= 22)
9314 |> extrude(length = 14)
9315";
9316 let mut frontend = FrontendState::new();
9317 frontend.program = Program::parse(initial_source).unwrap().0.unwrap();
9318 let outcome = ExecOutcome {
9319 variables: Default::default(),
9320 test_program_memory: Default::default(),
9321 operations: Default::default(),
9322 artifact_graph: Default::default(),
9323 scene_objects: Default::default(),
9324 source_range_to_object: Default::default(),
9325 var_solutions: Default::default(),
9326 refactor_metadata: Default::default(),
9327 issues: Default::default(),
9328 filenames: Default::default(),
9329 from_cache: false,
9330 source_files: Default::default(),
9331 default_planes: Default::default(),
9332 };
9333
9334 let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9335
9336 assert_eq!(source_delta.text, initial_source);
9337 }
9338
9339 #[tokio::test(flavor = "multi_thread")]
9342 async fn test_edit_segments_with_anchor_ids_limits_drag_fixed_constraints() {
9343 let initial_source = "\
9344sketch(on = XY) {
9345 point1 = point(at = [var 0mm, var 0mm])
9346 point2 = point(at = [var 0mm, var 0mm])
9347 coincident([point1, point2])
9348}
9349";
9350
9351 let program = Program::parse(initial_source).unwrap().0.unwrap();
9352 let mut frontend = FrontendState::new();
9353 let mock_ctx = ExecutorContext::new_mock(None).await;
9354 let version = Version(0);
9355
9356 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9357 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9358 let sketch_id = sketch_object.id;
9359 let sketch = expect_sketch(sketch_object);
9360 let point1_id = sketch.segments[0];
9361 let point2_id = sketch.segments[1];
9362
9363 let segments = vec![
9364 ExistingSegmentCtor {
9365 id: point1_id,
9366 ctor: SegmentCtor::Point(PointCtor {
9367 position: point_expr_mm(10.0, 0.0),
9368 }),
9369 },
9370 ExistingSegmentCtor {
9371 id: point2_id,
9372 ctor: SegmentCtor::Point(PointCtor {
9373 position: point_expr_mm(100.0, 0.0),
9374 }),
9375 },
9376 ];
9377 let (_, scene_delta) = frontend
9378 .edit_segments_with_options(
9379 &mock_ctx,
9380 version,
9381 sketch_id,
9382 segments,
9383 EditSegmentsOptions {
9384 anchor_segment_ids: Some(vec![point1_id]),
9385 drag_anchors: Vec::new(),
9386 constraint_label_edits: Vec::new(),
9387 commit_solved_initial_guesses: true,
9388 },
9389 )
9390 .await
9391 .unwrap();
9392
9393 assert_point_position_close(
9394 point_position(&scene_delta.new_graph, point1_id),
9395 point_number_mm(10.0, 0.0),
9396 );
9397 assert_point_position_close(
9398 point_position(&scene_delta.new_graph, point2_id),
9399 point_number_mm(10.0, 0.0),
9400 );
9401
9402 mock_ctx.close().await;
9403 }
9404
9405 fn collect_sketch_var_literals_with_value(program: &Program, value: f64) -> Vec<(SourceRange, ast::NodePath)> {
9408 use std::cell::RefCell;
9409 struct Collector {
9410 target: f64,
9411 out: RefCell<Vec<(SourceRange, ast::NodePath)>>,
9412 }
9413 impl<'a> crate::walk::Visitor<'a> for &Collector {
9414 type Error = crate::front::Error;
9415 fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
9416 if let crate::walk::Node::SketchVar(sketch_var) = node
9417 && let (Some(initial), Some(node_path)) = (&sketch_var.initial, &sketch_var.node_path)
9418 && (initial.value - self.target).abs() < 1e-9
9419 {
9420 self.out
9421 .borrow_mut()
9422 .push((SourceRange::from(initial.as_ref()), node_path.clone()));
9423 }
9424 for child in node.children().iter() {
9425 if !child.visit(*self)? {
9426 return Ok(false);
9427 }
9428 }
9429 Ok(true)
9430 }
9431 }
9432 let collector = Collector {
9433 target: value,
9434 out: Default::default(),
9435 };
9436 let _ = crate::walk::Node::from(&program.ast).visit(&collector);
9437 collector.out.into_inner()
9438 }
9439
9440 fn collect_all_sketch_vars(program: &Program) -> Vec<(SourceRange, ast::NodePath)> {
9443 use std::cell::RefCell;
9444 struct Collector {
9445 out: RefCell<Vec<(SourceRange, ast::NodePath)>>,
9446 }
9447 impl<'a> crate::walk::Visitor<'a> for &Collector {
9448 type Error = crate::front::Error;
9449 fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
9450 if let crate::walk::Node::SketchVar(sketch_var) = node
9451 && let Some(node_path) = &sketch_var.node_path
9452 {
9453 self.out
9454 .borrow_mut()
9455 .push((SourceRange::from(sketch_var), node_path.clone()));
9456 }
9457 for child in node.children().iter() {
9458 if !child.visit(*self)? {
9459 return Ok(false);
9460 }
9461 }
9462 Ok(true)
9463 }
9464 }
9465 let collector = Collector {
9466 out: Default::default(),
9467 };
9468 let _ = crate::walk::Node::from(&program.ast).visit(&collector);
9469 collector.out.into_inner()
9470 }
9471
9472 fn empty_exec_outcome_with_var_solutions(
9473 var_solutions: Vec<(SourceRange, Option<ast::NodePath>, Number)>,
9474 ) -> ExecOutcome {
9475 ExecOutcome {
9476 variables: Default::default(),
9477 test_program_memory: Default::default(),
9478 operations: Default::default(),
9479 artifact_graph: Default::default(),
9480 scene_objects: Default::default(),
9481 source_range_to_object: Default::default(),
9482 var_solutions,
9483 refactor_metadata: Default::default(),
9484 issues: Default::default(),
9485 filenames: Default::default(),
9486 from_cache: false,
9487 source_files: Default::default(),
9488 default_planes: Default::default(),
9489 }
9490 }
9491
9492 #[test]
9496 fn test_commit_var_solution_by_node_path_updates_sketch_var() {
9497 let initial_source = "\
9498sketch(on = XY) {
9499 line1 = line(start = [var 0, var 0], end = [var 10mm, var 0])
9500}
9501";
9502 let program = Program::parse(initial_source).unwrap().0.unwrap();
9503 let matches = collect_sketch_var_literals_with_value(&program, 10.0);
9504 assert_eq!(matches.len(), 1, "expected exactly one `var 10mm`");
9505 let (literal_range, node_path) = matches.into_iter().next().unwrap();
9506
9507 let mut frontend = FrontendState::new();
9508 frontend.program = program;
9509
9510 let outcome = empty_exec_outcome_with_var_solutions(vec![(
9511 literal_range,
9512 Some(node_path),
9513 Number {
9514 value: 25.0,
9515 units: NumericSuffix::Mm,
9516 },
9517 )]);
9518
9519 let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9520
9521 insta::assert_snapshot!(
9522 "test_commit_var_solution_by_node_path_updates_sketch_var",
9523 source_delta.text
9524 );
9525 }
9526
9527 #[test]
9534 fn test_commit_var_solution_survives_whitespace_shift_earlier_in_file() {
9535 let compact_source = "\
9536sketch(on = XY) {
9537 line1 = line(start = [var 0, var 0], end = [var 10mm, var 0])
9538}
9539";
9540 let padded_source = "\
9541// added comment\n// added comment\n\nsketch(on = XY) {
9542 line1 = line(start = [var 0, var 0], end = [var 10mm, var 0])
9543}
9544";
9545 let compact_program = Program::parse(compact_source).unwrap().0.unwrap();
9546 let padded_program = Program::parse(padded_source).unwrap().0.unwrap();
9547
9548 let compact_match = collect_sketch_var_literals_with_value(&compact_program, 10.0)
9549 .into_iter()
9550 .next()
9551 .expect("expected `var 10mm` in compact source");
9552 let padded_match = collect_sketch_var_literals_with_value(&padded_program, 10.0)
9553 .into_iter()
9554 .next()
9555 .expect("expected `var 10mm` in padded source");
9556
9557 assert_ne!(
9558 compact_match.0, padded_match.0,
9559 "byte offsets must differ for this test to be meaningful"
9560 );
9561 assert_eq!(
9562 compact_match.1, padded_match.1,
9563 "node paths must agree across whitespace; that's the whole point of NodePath",
9564 );
9565
9566 let mut frontend = FrontendState::new();
9567 frontend.program = padded_program;
9568
9569 let outcome = empty_exec_outcome_with_var_solutions(vec![(
9571 compact_match.0,
9572 Some(compact_match.1),
9573 Number {
9574 value: 30.0,
9575 units: NumericSuffix::Mm,
9576 },
9577 )]);
9578
9579 let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9580
9581 insta::assert_snapshot!(
9582 "test_commit_var_solution_survives_whitespace_shift_earlier_in_file",
9583 source_delta.text
9584 );
9585 }
9586
9587 #[test]
9591 fn test_commit_var_solution_node_path_wins_when_source_range_points_at_wrong_var() {
9592 let initial_source = "\
9593sketch(on = XY) {
9594 line1 = line(start = [var 10mm, var 0mm], end = [var 20mm, var 0mm])
9595}
9596";
9597 let program = Program::parse(initial_source).unwrap().0.unwrap();
9598
9599 let var_10 = collect_sketch_var_literals_with_value(&program, 10.0)
9600 .into_iter()
9601 .next()
9602 .expect("expected `var 10mm`");
9603 let var_20 = collect_sketch_var_literals_with_value(&program, 20.0)
9604 .into_iter()
9605 .next()
9606 .expect("expected `var 20mm`");
9607
9608 let mut frontend = FrontendState::new();
9609 frontend.program = program;
9610
9611 let outcome = empty_exec_outcome_with_var_solutions(vec![(
9613 var_20.0,
9614 Some(var_10.1),
9615 Number {
9616 value: 33.0,
9617 units: NumericSuffix::Mm,
9618 },
9619 )]);
9620
9621 let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9622
9623 insta::assert_snapshot!(
9624 "test_commit_var_solution_node_path_wins_when_source_range_points_at_wrong_var",
9625 source_delta.text
9626 );
9627 }
9628
9629 #[test]
9636 fn test_commit_var_solution_writes_back_into_bare_var() {
9637 let initial_source = "\
9638@settings(experimentalFeatures = allow, kclVersion = 2.0)
9639sketch(on = XY) {
9640 line1 = line(start = [var, var 0mm], end = [var 10mm, var 0])
9641}
9642";
9643 let program = Program::parse(initial_source).unwrap().0.unwrap();
9644
9645 let bare = collect_all_sketch_vars(&program)
9648 .into_iter()
9649 .find(|(range, _)| {
9650 range.end() - range.start() == 3
9652 })
9653 .expect("expected at least one bare `var`");
9654
9655 let mut frontend = FrontendState::new();
9656 frontend.program = program;
9657
9658 let outcome = empty_exec_outcome_with_var_solutions(vec![(
9659 bare.0,
9660 Some(bare.1),
9661 Number {
9662 value: 7.0,
9663 units: NumericSuffix::Mm,
9664 },
9665 )]);
9666
9667 let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9668
9669 insta::assert_snapshot!("test_commit_var_solution_writes_back_into_bare_var", source_delta.text);
9673 }
9674
9675 #[tokio::test(flavor = "multi_thread")]
9676 async fn test_delete_point_without_var() {
9677 let initial_source = "\
9678sketch(on = XY) {
9679 point(at = [var 1, var 2])
9680 point(at = [var 3, var 4])
9681 point(at = [var 5, var 6])
9682}
9683";
9684
9685 let program = Program::parse(initial_source).unwrap().0.unwrap();
9686
9687 let mut frontend = FrontendState::new();
9688
9689 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
9690 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
9691 let version = Version(0);
9692
9693 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9694 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9695 let sketch_id = sketch_object.id;
9696 let sketch = expect_sketch(sketch_object);
9697
9698 let point_id = *sketch.segments.get(1).unwrap();
9699
9700 let (src_delta, scene_delta) = frontend
9701 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
9702 .await
9703 .unwrap();
9704 insta::assert_snapshot!("test_delete_point_without_var", src_delta.text.as_str());
9705 assert_eq!(scene_delta.new_objects, vec![]);
9706 assert_eq!(scene_delta.new_graph.objects.len(), 4);
9707
9708 ctx.close().await;
9709 mock_ctx.close().await;
9710 }
9711
9712 #[tokio::test(flavor = "multi_thread")]
9713 async fn test_delete_point_with_var() {
9714 let initial_source = "\
9715sketch(on = XY) {
9716 point(at = [var 1, var 2])
9717 point1 = point(at = [var 3, var 4])
9718 point(at = [var 5, var 6])
9719}
9720";
9721
9722 let program = Program::parse(initial_source).unwrap().0.unwrap();
9723
9724 let mut frontend = FrontendState::new();
9725
9726 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
9727 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
9728 let version = Version(0);
9729
9730 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9731 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9732 let sketch_id = sketch_object.id;
9733 let sketch = expect_sketch(sketch_object);
9734
9735 let point_id = *sketch.segments.get(1).unwrap();
9736
9737 let (src_delta, scene_delta) = frontend
9738 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
9739 .await
9740 .unwrap();
9741 insta::assert_snapshot!("test_delete_point_with_var", src_delta.text.as_str());
9742 assert_eq!(scene_delta.new_objects, vec![]);
9743 assert_eq!(scene_delta.new_graph.objects.len(), 4);
9744
9745 ctx.close().await;
9746 mock_ctx.close().await;
9747 }
9748
9749 #[tokio::test(flavor = "multi_thread")]
9750 async fn test_delete_multiple_points() {
9751 let initial_source = "\
9752sketch(on = XY) {
9753 point(at = [var 1, var 2])
9754 point1 = point(at = [var 3, var 4])
9755 point(at = [var 5, var 6])
9756}
9757";
9758
9759 let program = Program::parse(initial_source).unwrap().0.unwrap();
9760
9761 let mut frontend = FrontendState::new();
9762
9763 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
9764 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
9765 let version = Version(0);
9766
9767 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9768 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9769 let sketch_id = sketch_object.id;
9770
9771 let sketch = expect_sketch(sketch_object);
9772
9773 let point1_id = *sketch.segments.first().unwrap();
9774 let point2_id = *sketch.segments.get(1).unwrap();
9775
9776 let (src_delta, scene_delta) = frontend
9777 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point1_id, point2_id])
9778 .await
9779 .unwrap();
9780 insta::assert_snapshot!("test_delete_multiple_points", src_delta.text.as_str());
9781 assert_eq!(scene_delta.new_objects, vec![]);
9782 assert_eq!(scene_delta.new_graph.objects.len(), 3);
9783
9784 ctx.close().await;
9785 mock_ctx.close().await;
9786 }
9787
9788 #[tokio::test(flavor = "multi_thread")]
9789 async fn test_delete_coincident_constraint() {
9790 let initial_source = "\
9791sketch(on = XY) {
9792 point1 = point(at = [var 1, var 2])
9793 point2 = point(at = [var 3, var 4])
9794 coincident([point1, point2])
9795 point(at = [var 5, var 6])
9796}
9797";
9798
9799 let program = Program::parse(initial_source).unwrap().0.unwrap();
9800
9801 let mut frontend = FrontendState::new();
9802
9803 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
9804 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
9805 let version = Version(0);
9806
9807 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9808 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9809 let sketch_id = sketch_object.id;
9810 let sketch = expect_sketch(sketch_object);
9811
9812 let coincident_id = *sketch.constraints.first().unwrap();
9813
9814 let (src_delta, scene_delta) = frontend
9815 .delete_objects(&mock_ctx, version, sketch_id, vec![coincident_id], Vec::new())
9816 .await
9817 .unwrap();
9818 insta::assert_snapshot!("test_delete_coincident_constraint", src_delta.text.as_str());
9819 assert_eq!(scene_delta.new_objects, vec![]);
9820 assert_eq!(scene_delta.new_graph.objects.len(), 5);
9821
9822 ctx.close().await;
9823 mock_ctx.close().await;
9824 }
9825
9826 #[tokio::test(flavor = "multi_thread")]
9827 async fn test_delete_line_cascades_to_coincident_constraint() {
9828 let initial_source = "\
9829sketch(on = XY) {
9830 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9831 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9832 coincident([line1.end, line2.start])
9833}
9834";
9835
9836 let program = Program::parse(initial_source).unwrap().0.unwrap();
9837
9838 let mut frontend = FrontendState::new();
9839
9840 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
9841 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
9842 let version = Version(0);
9843
9844 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9845 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9846 let sketch_id = sketch_object.id;
9847 let sketch = expect_sketch(sketch_object);
9848 let line_id = *sketch.segments.get(5).unwrap();
9849
9850 let (src_delta, scene_delta) = frontend
9851 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
9852 .await
9853 .unwrap();
9854 insta::assert_snapshot!(
9855 "test_delete_line_cascades_to_coincident_constraint",
9856 src_delta.text.as_str()
9857 );
9858 assert_eq!(
9859 scene_delta.new_graph.objects.len(),
9860 5,
9861 "{:#?}",
9862 scene_delta.new_graph.objects
9863 );
9864
9865 ctx.close().await;
9866 mock_ctx.close().await;
9867 }
9868
9869 #[tokio::test(flavor = "multi_thread")]
9870 async fn test_delete_line_cascades_to_distance_constraint() {
9871 let initial_source = "\
9872sketch(on = XY) {
9873 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9874 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9875 distance([line1.end, line2.start]) == 10mm
9876}
9877";
9878
9879 let program = Program::parse(initial_source).unwrap().0.unwrap();
9880
9881 let mut frontend = FrontendState::new();
9882
9883 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
9884 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
9885 let version = Version(0);
9886
9887 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9888 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9889 let sketch_id = sketch_object.id;
9890 let sketch = expect_sketch(sketch_object);
9891 let line_id = *sketch.segments.get(5).unwrap();
9892
9893 let (src_delta, scene_delta) = frontend
9894 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
9895 .await
9896 .unwrap();
9897 insta::assert_snapshot!(
9898 "test_delete_line_cascades_to_distance_constraint",
9899 src_delta.text.as_str()
9900 );
9901 assert_eq!(
9902 scene_delta.new_graph.objects.len(),
9903 5,
9904 "{:#?}",
9905 scene_delta.new_graph.objects
9906 );
9907
9908 ctx.close().await;
9909 mock_ctx.close().await;
9910 }
9911
9912 #[tokio::test(flavor = "multi_thread")]
9913 async fn test_delete_point_cascades_to_horizontal_distance_constraint() {
9914 let initial_source = "\
9915sketch(on = XY) {
9916 point1 = point(at = [var 1, var 2])
9917 point2 = point(at = [var 3, var 4])
9918 horizontalDistance([point1, point2]) == 10mm
9919}
9920";
9921
9922 let program = Program::parse(initial_source).unwrap().0.unwrap();
9923
9924 let mut frontend = FrontendState::new();
9925
9926 let mock_ctx = ExecutorContext::new_mock(None).await;
9927 let version = Version(0);
9928
9929 frontend.program = program.clone();
9930 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9931 frontend.update_state_after_exec(outcome, true);
9932 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9933 let sketch_id = sketch_object.id;
9934 let sketch = expect_sketch(sketch_object);
9935 let point2_id = *sketch.segments.get(1).unwrap();
9936
9937 let (src_delta, scene_delta) = frontend
9938 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point2_id])
9939 .await
9940 .unwrap();
9941 insta::assert_snapshot!(
9942 "test_delete_point_cascades_to_horizontal_distance_constraint",
9943 src_delta.text.as_str()
9944 );
9945 assert_eq!(
9946 scene_delta.new_graph.objects.len(),
9947 3,
9948 "{:#?}",
9949 scene_delta.new_graph.objects
9950 );
9951
9952 mock_ctx.close().await;
9953 }
9954
9955 #[tokio::test(flavor = "multi_thread")]
9956 async fn test_delete_line_cascades_to_fixed_constraint() {
9957 let initial_source = "\
9958sketch(on = XY) {
9959 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9960 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9961 fixed([line1.start, [0, 0]])
9962}
9963";
9964
9965 let program = Program::parse(initial_source).unwrap().0.unwrap();
9966
9967 let mut frontend = FrontendState::new();
9968
9969 let mock_ctx = ExecutorContext::new_mock(None).await;
9970 let version = Version(0);
9971
9972 frontend.program = program.clone();
9973 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9974 frontend.update_state_after_exec(outcome, true);
9975 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9976 let sketch_id = sketch_object.id;
9977 let sketch = expect_sketch(sketch_object);
9978 let line1_id = *sketch.segments.get(2).unwrap();
9979
9980 let (src_delta, scene_delta) = frontend
9981 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
9982 .await
9983 .unwrap();
9984 insta::assert_snapshot!("test_delete_line_cascades_to_fixed_constraint", src_delta.text.as_str());
9985 assert_eq!(
9986 scene_delta.new_graph.objects.len(),
9987 5,
9988 "{:#?}",
9989 scene_delta.new_graph.objects
9990 );
9991
9992 mock_ctx.close().await;
9993 }
9994
9995 #[tokio::test(flavor = "multi_thread")]
9996 async fn test_delete_line_cascades_to_midpoint_constraint() {
9997 let initial_source = "\
9998sketch(on = XY) {
9999 point1 = point(at = [var 1, var 2])
10000 line1 = line(start = [var 0, var 0], end = [var 6, var 4])
10001 midpoint(line1, point = point1)
10002}
10003";
10004
10005 let program = Program::parse(initial_source).unwrap().0.unwrap();
10006
10007 let mut frontend = FrontendState::new();
10008
10009 let mock_ctx = ExecutorContext::new_mock(None).await;
10010 let version = Version(0);
10011
10012 frontend.program = program.clone();
10013 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10014 frontend.update_state_after_exec(outcome, true);
10015 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10016 let sketch_id = sketch_object.id;
10017 let sketch = expect_sketch(sketch_object);
10018 let line1_id = *sketch.segments.get(3).unwrap();
10019
10020 let (src_delta, scene_delta) = frontend
10021 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
10022 .await
10023 .unwrap();
10024 insta::assert_snapshot!(
10025 "test_delete_line_cascades_to_midpoint_constraint",
10026 src_delta.text.as_str()
10027 );
10028 assert_eq!(
10029 scene_delta.new_graph.objects.len(),
10030 3,
10031 "{:#?}",
10032 scene_delta.new_graph.objects
10033 );
10034
10035 mock_ctx.close().await;
10036 }
10037
10038 #[tokio::test(flavor = "multi_thread")]
10039 async fn test_delete_point_preserves_multiline_coincident_constraint() {
10040 let initial_source = "\
10041sketch(on = XY) {
10042 point1 = point(at = [var 1, var 2])
10043 point2 = point(at = [var 3, var 4])
10044 point3 = point(at = [var 5, var 6])
10045 coincident([point1, point2, point3])
10046}
10047";
10048
10049 let program = Program::parse(initial_source).unwrap().0.unwrap();
10050
10051 let mut frontend = FrontendState::new();
10052
10053 let mock_ctx = ExecutorContext::new_mock(None).await;
10054 let version = Version(0);
10055
10056 frontend.program = program.clone();
10057 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10058 frontend.update_state_after_exec(outcome, true);
10059 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10060 let sketch_id = sketch_object.id;
10061 let sketch = expect_sketch(sketch_object);
10062 let point3_id = *sketch.segments.get(2).unwrap();
10063
10064 let (src_delta, scene_delta) = frontend
10065 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point3_id])
10066 .await
10067 .unwrap();
10068 assert!(src_delta.text.contains("point1 = point("), "{}", src_delta.text);
10069 assert!(src_delta.text.contains("point2 = point("), "{}", src_delta.text);
10070 assert!(!src_delta.text.contains("point3 = point("), "{}", src_delta.text);
10071 assert!(
10072 src_delta.text.contains("coincident([point1, point2])"),
10073 "{}",
10074 src_delta.text
10075 );
10076
10077 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10078 let sketch = expect_sketch(sketch_object);
10079 assert_eq!(sketch.segments.len(), 2);
10080 assert_eq!(sketch.constraints.len(), 1);
10081
10082 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10083 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10084 panic!("Expected constraint object");
10085 };
10086 let Constraint::Coincident(coincident) = constraint else {
10087 panic!("Expected coincident constraint");
10088 };
10089 assert_eq!(
10090 coincident.segments,
10091 sketch
10092 .segments
10093 .iter()
10094 .copied()
10095 .map(Into::into)
10096 .collect::<Vec<ConstraintSegment>>()
10097 );
10098
10099 mock_ctx.close().await;
10100 }
10101
10102 #[tokio::test(flavor = "multi_thread")]
10103 async fn test_delete_line_preserves_multiline_equal_length_constraint() {
10104 let initial_source = "\
10105sketch(on = XY) {
10106 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10107 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10108 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10109 equalLength([line1, line2, line3])
10110}
10111";
10112
10113 let program = Program::parse(initial_source).unwrap().0.unwrap();
10114
10115 let mut frontend = FrontendState::new();
10116
10117 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
10118 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
10119 let version = Version(0);
10120
10121 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10122 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10123 let sketch_id = sketch_object.id;
10124 let sketch = expect_sketch(sketch_object);
10125 let line3_id = *sketch.segments.get(8).unwrap();
10126
10127 let (src_delta, scene_delta) = frontend
10128 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line3_id])
10129 .await
10130 .unwrap();
10131 insta::assert_snapshot!(
10132 "test_delete_line_preserves_multiline_equal_length_constraint",
10133 src_delta.text.as_str()
10134 );
10135
10136 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10137 let sketch = expect_sketch(sketch_object);
10138 assert_eq!(sketch.constraints.len(), 1);
10139
10140 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10141 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10142 panic!("Expected constraint object");
10143 };
10144 let Constraint::LinesEqualLength(lines_equal_length) = constraint else {
10145 panic!("Expected lines equal length constraint");
10146 };
10147 assert_eq!(lines_equal_length.lines.len(), 2);
10148
10149 ctx.close().await;
10150 mock_ctx.close().await;
10151 }
10152
10153 #[tokio::test(flavor = "multi_thread")]
10154 async fn test_delete_line_preserves_multiline_horizontal_constraint() {
10155 let initial_source = "\
10156sketch(on = XY) {
10157 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10158 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10159 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10160 horizontal([line1.end, line2.start, line3.start])
10161}
10162";
10163
10164 let program = Program::parse(initial_source).unwrap().0.unwrap();
10165
10166 let mut frontend = FrontendState::new();
10167
10168 let mock_ctx = ExecutorContext::new_mock(None).await;
10169 let version = Version(0);
10170
10171 frontend.program = program.clone();
10172 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10173 frontend.update_state_after_exec(outcome, true);
10174 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10175 let sketch_id = sketch_object.id;
10176 let sketch = expect_sketch(sketch_object);
10177 let line1_id = *sketch.segments.get(2).unwrap();
10178
10179 let (src_delta, scene_delta) = frontend
10180 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
10181 .await
10182 .unwrap();
10183 assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
10184 assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
10185 assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
10186 assert!(
10187 src_delta.text.contains("horizontal([line2.start, line3.start])"),
10188 "{}",
10189 src_delta.text
10190 );
10191
10192 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10193 let sketch = expect_sketch(sketch_object);
10194 assert_eq!(sketch.constraints.len(), 1);
10195
10196 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10197 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10198 panic!("Expected constraint object");
10199 };
10200 let Constraint::Horizontal(Horizontal::Points { points }) = constraint else {
10201 panic!("Expected horizontal points constraint");
10202 };
10203 let remaining_points = vec![sketch.segments[0].into(), sketch.segments[3].into()];
10204 assert_eq!(*points, remaining_points);
10205
10206 mock_ctx.close().await;
10207 }
10208
10209 #[tokio::test(flavor = "multi_thread")]
10210 async fn test_delete_line_preserves_multiline_vertical_constraint() {
10211 let initial_source = "\
10212sketch(on = XY) {
10213 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10214 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10215 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10216 vertical([line1.end, line2.start, line3.start])
10217}
10218";
10219
10220 let program = Program::parse(initial_source).unwrap().0.unwrap();
10221
10222 let mut frontend = FrontendState::new();
10223
10224 let mock_ctx = ExecutorContext::new_mock(None).await;
10225 let version = Version(0);
10226
10227 frontend.program = program.clone();
10228 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10229 frontend.update_state_after_exec(outcome, true);
10230 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10231 let sketch_id = sketch_object.id;
10232 let sketch = expect_sketch(sketch_object);
10233 let line1_id = *sketch.segments.get(2).unwrap();
10234
10235 let (src_delta, scene_delta) = frontend
10236 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
10237 .await
10238 .unwrap();
10239 assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
10240 assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
10241 assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
10242 assert!(
10243 src_delta.text.contains("vertical([line2.start, line3.start])"),
10244 "{}",
10245 src_delta.text
10246 );
10247
10248 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10249 let sketch = expect_sketch(sketch_object);
10250 assert_eq!(sketch.constraints.len(), 1);
10251
10252 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10253 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10254 panic!("Expected constraint object");
10255 };
10256 let Constraint::Vertical(Vertical::Points { points }) = constraint else {
10257 panic!("Expected vertical points constraint");
10258 };
10259 let remaining_points = vec![sketch.segments[0].into(), sketch.segments[3].into()];
10260 assert_eq!(*points, remaining_points);
10261
10262 mock_ctx.close().await;
10263 }
10264
10265 #[tokio::test(flavor = "multi_thread")]
10266 async fn test_delete_line_preserves_multiline_coincident_constraint() {
10267 let initial_source = "\
10268sketch(on = XY) {
10269 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10270 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10271 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10272 coincident([line1.end, line2.start, line3.start])
10273}
10274";
10275
10276 let program = Program::parse(initial_source).unwrap().0.unwrap();
10277
10278 let mut frontend = FrontendState::new();
10279
10280 let mock_ctx = ExecutorContext::new_mock(None).await;
10281 let version = Version(0);
10282
10283 frontend.program = program.clone();
10284 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10285 frontend.update_state_after_exec(outcome, true);
10286 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10287 let sketch_id = sketch_object.id;
10288 let sketch = expect_sketch(sketch_object);
10289 let line1_id = *sketch.segments.get(2).unwrap();
10290
10291 let (src_delta, scene_delta) = frontend
10292 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
10293 .await
10294 .unwrap();
10295 assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
10296 assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
10297 assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
10298 assert!(
10299 src_delta.text.contains("coincident([line2.start, line3.start])"),
10300 "{}",
10301 src_delta.text
10302 );
10303
10304 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10305 let sketch = expect_sketch(sketch_object);
10306 assert_eq!(sketch.constraints.len(), 1);
10307
10308 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10309 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10310 panic!("Expected constraint object");
10311 };
10312 let Constraint::Coincident(coincident) = constraint else {
10313 panic!("Expected coincident constraint");
10314 };
10315 let remaining_segments = vec![sketch.segments[0].into(), sketch.segments[3].into()];
10316 assert_eq!(coincident.segments, remaining_segments);
10317
10318 mock_ctx.close().await;
10319 }
10320
10321 #[tokio::test(flavor = "multi_thread")]
10322 async fn test_delete_lines_removes_multiline_equal_length_constraint_below_minimum() {
10323 let initial_source = "\
10324sketch(on = XY) {
10325 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10326 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10327 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10328 equalLength([line1, line2, line3])
10329}
10330";
10331
10332 let program = Program::parse(initial_source).unwrap().0.unwrap();
10333
10334 let mut frontend = FrontendState::new();
10335
10336 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
10337 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
10338 let version = Version(0);
10339
10340 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10341 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10342 let sketch_id = sketch_object.id;
10343 let sketch = expect_sketch(sketch_object);
10344 let line2_id = *sketch.segments.get(5).unwrap();
10345 let line3_id = *sketch.segments.get(8).unwrap();
10346
10347 let (src_delta, scene_delta) = frontend
10348 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line2_id, line3_id])
10349 .await
10350 .unwrap();
10351 insta::assert_snapshot!(
10352 "test_delete_lines_removes_multiline_equal_length_constraint_below_minimum",
10353 src_delta.text.as_str()
10354 );
10355
10356 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10357 let sketch = expect_sketch(sketch_object);
10358 assert!(sketch.constraints.is_empty());
10359
10360 ctx.close().await;
10361 mock_ctx.close().await;
10362 }
10363
10364 #[tokio::test(flavor = "multi_thread")]
10365 async fn test_delete_line_preserves_multiline_parallel_constraint() {
10366 let initial_source = "\
10367sketch(on = XY) {
10368 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10369 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10370 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10371 parallel([line1, line2, line3])
10372}
10373";
10374
10375 let program = Program::parse(initial_source).unwrap().0.unwrap();
10376
10377 let mut frontend = FrontendState::new();
10378
10379 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
10380 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
10381 let version = Version(0);
10382
10383 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10384 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10385 let sketch_id = sketch_object.id;
10386 let sketch = expect_sketch(sketch_object);
10387 let line3_id = *sketch.segments.get(8).unwrap();
10388
10389 let (src_delta, scene_delta) = frontend
10390 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line3_id])
10391 .await
10392 .unwrap();
10393 insta::assert_snapshot!(
10394 "test_delete_line_preserves_multiline_parallel_constraint",
10395 src_delta.text.as_str()
10396 );
10397
10398 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10399 let sketch = expect_sketch(sketch_object);
10400 assert_eq!(sketch.constraints.len(), 1);
10401
10402 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10403 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10404 panic!("Expected constraint object");
10405 };
10406 let Constraint::Parallel(parallel) = constraint else {
10407 panic!("Expected parallel constraint");
10408 };
10409 assert_eq!(parallel.lines.len(), 2);
10410
10411 ctx.close().await;
10412 mock_ctx.close().await;
10413 }
10414
10415 #[tokio::test(flavor = "multi_thread")]
10416 async fn test_delete_lines_removes_multiline_parallel_constraint_below_minimum() {
10417 let initial_source = "\
10418sketch(on = XY) {
10419 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10420 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10421 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10422 parallel([line1, line2, line3])
10423}
10424";
10425
10426 let program = Program::parse(initial_source).unwrap().0.unwrap();
10427
10428 let mut frontend = FrontendState::new();
10429
10430 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
10431 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
10432 let version = Version(0);
10433
10434 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10435 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10436 let sketch_id = sketch_object.id;
10437 let sketch = expect_sketch(sketch_object);
10438 let line2_id = *sketch.segments.get(5).unwrap();
10439 let line3_id = *sketch.segments.get(8).unwrap();
10440
10441 let (src_delta, scene_delta) = frontend
10442 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line2_id, line3_id])
10443 .await
10444 .unwrap();
10445 insta::assert_snapshot!(
10446 "test_delete_lines_removes_multiline_parallel_constraint_below_minimum",
10447 src_delta.text.as_str()
10448 );
10449
10450 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10451 let sketch = expect_sketch(sketch_object);
10452 assert!(sketch.constraints.is_empty());
10453
10454 ctx.close().await;
10455 mock_ctx.close().await;
10456 }
10457
10458 #[tokio::test(flavor = "multi_thread")]
10459 async fn test_delete_line_line_coincident_constraint() {
10460 let initial_source = "\
10461sketch(on = XY) {
10462 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10463 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10464 coincident([line1, line2])
10465}
10466";
10467
10468 let program = Program::parse(initial_source).unwrap().0.unwrap();
10469
10470 let mut frontend = FrontendState::new();
10471
10472 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
10473 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
10474 let version = Version(0);
10475
10476 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10477 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10478 let sketch_id = sketch_object.id;
10479 let sketch = expect_sketch(sketch_object);
10480
10481 let coincident_id = *sketch.constraints.first().unwrap();
10482
10483 let (src_delta, scene_delta) = frontend
10484 .delete_objects(&mock_ctx, version, sketch_id, vec![coincident_id], Vec::new())
10485 .await
10486 .unwrap();
10487 insta::assert_snapshot!("test_delete_line_line_coincident_constraint", src_delta.text.as_str());
10488 assert_eq!(scene_delta.new_objects, vec![]);
10489 assert_eq!(scene_delta.new_graph.objects.len(), 8);
10490
10491 ctx.close().await;
10492 mock_ctx.close().await;
10493 }
10494
10495 #[tokio::test(flavor = "multi_thread")]
10496 async fn test_two_points_coincident() {
10497 let initial_source = "\
10498sketch(on = XY) {
10499 point1 = point(at = [var 1, var 2])
10500 point(at = [3, 4])
10501}
10502";
10503
10504 let program = Program::parse(initial_source).unwrap().0.unwrap();
10505
10506 let mut frontend = FrontendState::new();
10507
10508 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
10509 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
10510 let version = Version(0);
10511
10512 frontend.hack_set_program(&ctx, program).await.unwrap();
10513 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10514 let sketch_id = sketch_object.id;
10515 let sketch = expect_sketch(sketch_object);
10516 let point0_id = *sketch.segments.first().unwrap();
10517 let point1_id = *sketch.segments.get(1).unwrap();
10518
10519 let constraint = Constraint::Coincident(Coincident {
10520 segments: vec![point0_id.into(), point1_id.into()],
10521 });
10522 let (src_delta, scene_delta) = frontend
10523 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10524 .await
10525 .unwrap();
10526 insta::assert_snapshot!("test_two_points_coincident", src_delta.text.as_str());
10527 assert_eq!(
10528 scene_delta.new_graph.objects.len(),
10529 5,
10530 "{:#?}",
10531 scene_delta.new_graph.objects
10532 );
10533
10534 ctx.close().await;
10535 mock_ctx.close().await;
10536 }
10537
10538 #[tokio::test(flavor = "multi_thread")]
10539 async fn test_three_points_coincident() {
10540 let initial_source = "\
10541sketch(on = XY) {
10542 point1 = point(at = [var 1, var 2])
10543 point(at = [var 3, var 4])
10544 point(at = [var 5, var 6])
10545}
10546";
10547
10548 let program = Program::parse(initial_source).unwrap().0.unwrap();
10549
10550 let mut frontend = FrontendState::new();
10551
10552 let mock_ctx = ExecutorContext::new_mock(None).await;
10553 let version = Version(0);
10554
10555 frontend.program = program.clone();
10556 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10557 frontend.update_state_after_exec(outcome, true);
10558 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10559 let sketch_id = sketch_object.id;
10560 let sketch = expect_sketch(sketch_object);
10561 let segments = sketch
10562 .segments
10563 .iter()
10564 .take(3)
10565 .copied()
10566 .map(Into::into)
10567 .collect::<Vec<ConstraintSegment>>();
10568
10569 let constraint = Constraint::Coincident(Coincident {
10570 segments: segments.clone(),
10571 });
10572 let (src_delta, scene_delta) = frontend
10573 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10574 .await
10575 .unwrap();
10576 insta::assert_snapshot!("test_three_points_coincident", src_delta.text.as_str());
10577
10578 let constraint_object = scene_delta
10579 .new_graph
10580 .objects
10581 .iter()
10582 .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
10583 .unwrap();
10584
10585 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10586 panic!("expected a constraint object");
10587 };
10588
10589 assert_eq!(constraint, &Constraint::Coincident(Coincident { segments }));
10590
10591 mock_ctx.close().await;
10592 }
10593
10594 #[tokio::test(flavor = "multi_thread")]
10595 async fn test_source_with_three_point_coincident_tracks_all_segments() {
10596 let initial_source = "\
10597sketch(on = XY) {
10598 point1 = point(at = [var 1, var 2])
10599 point2 = point(at = [var 3, var 4])
10600 point3 = point(at = [var 5, var 6])
10601 coincident([point1, point2, point3])
10602}
10603";
10604
10605 let program = Program::parse(initial_source).unwrap().0.unwrap();
10606
10607 let mut frontend = FrontendState::new();
10608
10609 let ctx = ExecutorContext::new_mock(None).await;
10610 frontend.program = program.clone();
10611 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10612 frontend.update_state_after_exec(outcome, true);
10613
10614 let constraint_object = frontend
10615 .scene_graph
10616 .objects
10617 .iter()
10618 .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
10619 .unwrap();
10620 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10621 panic!("expected a constraint object");
10622 };
10623
10624 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10625 let sketch = expect_sketch(sketch_object);
10626 let expected_segments = sketch
10627 .segments
10628 .iter()
10629 .take(3)
10630 .copied()
10631 .map(Into::into)
10632 .collect::<Vec<ConstraintSegment>>();
10633
10634 assert_eq!(
10635 constraint,
10636 &Constraint::Coincident(Coincident {
10637 segments: expected_segments,
10638 })
10639 );
10640
10641 ctx.close().await;
10642 }
10643
10644 #[tokio::test(flavor = "multi_thread")]
10645 async fn test_point_origin_coincident_preserves_order() {
10646 let initial_source = "\
10647sketch(on = XY) {
10648 point(at = [var 1, var 2])
10649}
10650";
10651
10652 for (origin_first, snapshot_name) in [
10653 (true, "test_point_origin_coincident_preserves_order_origin_first"),
10654 (false, "test_point_origin_coincident_preserves_order_point_first"),
10655 ] {
10656 let program = Program::parse(initial_source).unwrap().0.unwrap();
10657
10658 let mut frontend = FrontendState::new();
10659
10660 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
10661 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
10662 let version = Version(0);
10663
10664 frontend.hack_set_program(&ctx, program).await.unwrap();
10665 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10666 let sketch_id = sketch_object.id;
10667 let sketch = expect_sketch(sketch_object);
10668 let point_id = *sketch.segments.first().unwrap();
10669
10670 let segments = if origin_first {
10671 vec![ConstraintSegment::ORIGIN, point_id.into()]
10672 } else {
10673 vec![point_id.into(), ConstraintSegment::ORIGIN]
10674 };
10675 let constraint = Constraint::Coincident(Coincident {
10676 segments: segments.clone(),
10677 });
10678 let (src_delta, scene_delta) = frontend
10679 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10680 .await
10681 .unwrap();
10682 insta::assert_snapshot!(snapshot_name, src_delta.text.as_str());
10683
10684 let constraint_object = scene_delta
10685 .new_graph
10686 .objects
10687 .iter()
10688 .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
10689 .unwrap();
10690
10691 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10692 panic!("expected a constraint object");
10693 };
10694
10695 assert_eq!(constraint, &Constraint::Coincident(Coincident { segments }));
10696
10697 ctx.close().await;
10698 mock_ctx.close().await;
10699 }
10700 }
10701
10702 #[tokio::test(flavor = "multi_thread")]
10703 async fn test_coincident_of_line_end_points() {
10704 let initial_source = "\
10705sketch(on = XY) {
10706 line(start = [var 1, var 2], end = [var 3, var 4])
10707 line(start = [var 5, var 6], end = [var 7, var 8])
10708}
10709";
10710
10711 let program = Program::parse(initial_source).unwrap().0.unwrap();
10712
10713 let mut frontend = FrontendState::new();
10714
10715 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
10716 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
10717 let version = Version(0);
10718
10719 frontend.hack_set_program(&ctx, program).await.unwrap();
10720 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10721 let sketch_id = sketch_object.id;
10722 let sketch = expect_sketch(sketch_object);
10723 let point0_id = *sketch.segments.get(1).unwrap();
10724 let point1_id = *sketch.segments.get(3).unwrap();
10725
10726 let constraint = Constraint::Coincident(Coincident {
10727 segments: vec![point0_id.into(), point1_id.into()],
10728 });
10729 let (src_delta, scene_delta) = frontend
10730 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10731 .await
10732 .unwrap();
10733 insta::assert_snapshot!("test_coincident_of_line_end_points", src_delta.text.as_str());
10734 assert_eq!(
10735 scene_delta.new_graph.objects.len(),
10736 9,
10737 "{:#?}",
10738 scene_delta.new_graph.objects
10739 );
10740
10741 ctx.close().await;
10742 mock_ctx.close().await;
10743 }
10744
10745 #[tokio::test(flavor = "multi_thread")]
10746 async fn test_coincident_of_line_point_and_circle_segment() {
10747 let initial_source = "\
10748sketch(on = XY) {
10749 circle1 = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
10750 line1 = line(start = [var 9mm, var 1mm], end = [var 10mm, var 2mm])
10751}
10752";
10753 let program = Program::parse(initial_source).unwrap().0.unwrap();
10754 let mut frontend = FrontendState::new();
10755
10756 let mock_ctx = ExecutorContext::new_mock(None).await;
10757 let version = Version(0);
10758
10759 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10760 frontend.program = program;
10761 frontend.update_state_after_exec(outcome, true);
10762 let sketch_object = find_first_sketch_object(&frontend.scene_graph).expect("Expected sketch object");
10763 let sketch_id = sketch_object.id;
10764 let sketch = expect_sketch(sketch_object);
10765
10766 let circle_id = sketch
10767 .segments
10768 .iter()
10769 .copied()
10770 .find(|seg_id| {
10771 matches!(
10772 &frontend.scene_graph.objects[seg_id.0].kind,
10773 ObjectKind::Segment {
10774 segment: Segment::Circle(_)
10775 }
10776 )
10777 })
10778 .expect("Expected a circle segment in sketch");
10779 let line_id = frontend
10780 .scene_graph
10781 .objects
10782 .iter()
10783 .find_map(|obj| match &obj.kind {
10784 ObjectKind::Segment {
10785 segment: Segment::Line(line),
10786 } if line.owner.is_none() => Some(obj.id),
10787 _ => None,
10788 })
10789 .expect("Expected a standalone line segment in scene graph");
10790
10791 let line_start_point_id = match &frontend.scene_graph.objects[line_id.0].kind {
10792 ObjectKind::Segment {
10793 segment: Segment::Line(line),
10794 } => line.start,
10795 _ => panic!("Expected line segment object"),
10796 };
10797
10798 let constraint = Constraint::Coincident(Coincident {
10799 segments: vec![line_start_point_id.into(), circle_id.into()],
10800 });
10801 let (src_delta, _scene_delta) = frontend
10802 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10803 .await
10804 .unwrap();
10805 insta::assert_snapshot!(
10806 "test_coincident_of_line_point_and_circle_segment",
10807 src_delta.text.as_str()
10808 );
10809
10810 mock_ctx.close().await;
10811 }
10812
10813 #[tokio::test(flavor = "multi_thread")]
10814 async fn test_invalid_coincident_arc_and_line_preserves_state() {
10815 let program = Program::empty();
10823
10824 let mut frontend = FrontendState::new();
10825 frontend.program = program;
10826
10827 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
10828 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
10829 let version = Version(0);
10830
10831 let sketch_args = SketchCtor {
10832 on: Plane::Default(PlaneName::Xy),
10833 };
10834 let (_src_delta, _scene_delta, sketch_id) = frontend
10835 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
10836 .await
10837 .unwrap();
10838
10839 let arc_ctor = ArcCtor {
10841 start: Point2d {
10842 x: Expr::Var(Number {
10843 value: 0.0,
10844 units: NumericSuffix::Mm,
10845 }),
10846 y: Expr::Var(Number {
10847 value: 0.0,
10848 units: NumericSuffix::Mm,
10849 }),
10850 },
10851 end: Point2d {
10852 x: Expr::Var(Number {
10853 value: 10.0,
10854 units: NumericSuffix::Mm,
10855 }),
10856 y: Expr::Var(Number {
10857 value: 10.0,
10858 units: NumericSuffix::Mm,
10859 }),
10860 },
10861 center: Point2d {
10862 x: Expr::Var(Number {
10863 value: 10.0,
10864 units: NumericSuffix::Mm,
10865 }),
10866 y: Expr::Var(Number {
10867 value: 0.0,
10868 units: NumericSuffix::Mm,
10869 }),
10870 },
10871 direction: None,
10872 construction: None,
10873 };
10874 let (_src_delta, scene_delta) = frontend
10875 .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Arc(arc_ctor), None)
10876 .await
10877 .unwrap();
10878 let arc_id = *scene_delta.new_objects.last().unwrap();
10880
10881 let line_ctor = LineCtor {
10883 start: Point2d {
10884 x: Expr::Var(Number {
10885 value: 20.0,
10886 units: NumericSuffix::Mm,
10887 }),
10888 y: Expr::Var(Number {
10889 value: 0.0,
10890 units: NumericSuffix::Mm,
10891 }),
10892 },
10893 end: Point2d {
10894 x: Expr::Var(Number {
10895 value: 30.0,
10896 units: NumericSuffix::Mm,
10897 }),
10898 y: Expr::Var(Number {
10899 value: 10.0,
10900 units: NumericSuffix::Mm,
10901 }),
10902 },
10903 construction: None,
10904 };
10905 let (_src_delta, scene_delta) = frontend
10906 .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Line(line_ctor), None)
10907 .await
10908 .unwrap();
10909 let line_id = *scene_delta.new_objects.last().unwrap();
10911
10912 let constraint = Constraint::Coincident(Coincident {
10915 segments: vec![arc_id.into(), line_id.into()],
10916 });
10917 let result = frontend.add_constraint(&mock_ctx, version, sketch_id, constraint).await;
10918
10919 assert!(result.is_err(), "Expected invalid coincident constraint to fail");
10921
10922 let sketch_object_after =
10925 find_first_sketch_object(&frontend.scene_graph).expect("Sketch should still exist after failed constraint");
10926 let sketch_after = expect_sketch(sketch_object_after);
10927
10928 assert!(
10930 sketch_after.segments.contains(&arc_id),
10931 "Arc segment should still exist after failed constraint"
10932 );
10933 assert!(
10934 sketch_after.segments.contains(&line_id),
10935 "Line segment should still exist after failed constraint"
10936 );
10937
10938 let arc_obj = frontend
10940 .scene_graph
10941 .objects
10942 .get(arc_id.0)
10943 .expect("Arc object should still be accessible");
10944 let line_obj = frontend
10945 .scene_graph
10946 .objects
10947 .get(line_id.0)
10948 .expect("Line object should still be accessible");
10949
10950 match &arc_obj.kind {
10953 ObjectKind::Segment {
10954 segment: Segment::Arc(_),
10955 } => {}
10956 _ => panic!("Arc object should still be an arc segment"),
10957 }
10958 match &line_obj.kind {
10959 ObjectKind::Segment {
10960 segment: Segment::Line(_),
10961 } => {}
10962 _ => panic!("Line object should still be a line segment"),
10963 }
10964
10965 ctx.close().await;
10966 mock_ctx.close().await;
10967 }
10968
10969 #[tokio::test(flavor = "multi_thread")]
10970 async fn test_distance_two_points() {
10971 let initial_source = "\
10972sketch(on = XY) {
10973 point(at = [var 1, var 2])
10974 point(at = [var 3, var 4])
10975}
10976";
10977
10978 let program = Program::parse(initial_source).unwrap().0.unwrap();
10979
10980 let mut frontend = FrontendState::new();
10981
10982 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
10983 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
10984 let version = Version(0);
10985
10986 frontend.hack_set_program(&ctx, program).await.unwrap();
10987 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10988 let sketch_id = sketch_object.id;
10989 let sketch = expect_sketch(sketch_object);
10990 let point0_id = *sketch.segments.first().unwrap();
10991 let point1_id = *sketch.segments.get(1).unwrap();
10992
10993 let constraint = Constraint::Distance(Distance {
10994 segments: vec![point0_id.into(), point1_id.into()],
10995 distance: Number {
10996 value: 2.0,
10997 units: NumericSuffix::Mm,
10998 },
10999 label_position: None,
11000 source: Default::default(),
11001 });
11002 let (src_delta, scene_delta) = frontend
11003 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11004 .await
11005 .unwrap();
11006 insta::assert_snapshot!("test_distance_two_points", src_delta.text.as_str());
11007 assert_eq!(
11008 scene_delta.new_graph.objects.len(),
11009 5,
11010 "{:#?}",
11011 scene_delta.new_graph.objects
11012 );
11013
11014 ctx.close().await;
11015 mock_ctx.close().await;
11016 }
11017
11018 #[tokio::test(flavor = "multi_thread")]
11019 async fn test_distance_two_points_with_label() {
11020 let initial_source = "\
11021sketch(on = XY) {
11022 point(at = [var 1, var 2])
11023 point(at = [var 3, var 4])
11024}
11025";
11026
11027 let program = Program::parse(initial_source).unwrap().0.unwrap();
11028
11029 let mut frontend = FrontendState::new();
11030
11031 let mock_ctx = ExecutorContext::new_mock(None).await;
11032 let version = Version(0);
11033
11034 frontend.program = program.clone();
11035 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11036 frontend.update_state_after_exec(outcome, true);
11037 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11038 let sketch_id = sketch_object.id;
11039 let sketch = expect_sketch(sketch_object);
11040 let point0_id = *sketch.segments.first().unwrap();
11041 let point1_id = *sketch.segments.get(1).unwrap();
11042
11043 let label_position = Point2d {
11044 x: Number {
11045 value: 10.0,
11046 units: NumericSuffix::Mm,
11047 },
11048 y: Number {
11049 value: 11.0,
11050 units: NumericSuffix::Mm,
11051 },
11052 };
11053 let constraint = Constraint::Distance(Distance {
11054 segments: vec![point0_id.into(), point1_id.into()],
11055 distance: Number {
11056 value: 2.0,
11057 units: NumericSuffix::Mm,
11058 },
11059 label_position: Some(label_position.clone()),
11060 source: Default::default(),
11061 });
11062 let (src_delta, scene_delta) = frontend
11063 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11064 .await
11065 .unwrap();
11066 insta::assert_snapshot!("test_distance_two_points_with_label", src_delta.text.as_str());
11067
11068 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
11069 let sketch = expect_sketch(sketch_object);
11070 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
11071 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11072 panic!("Expected constraint object");
11073 };
11074 let Constraint::Distance(distance) = constraint else {
11075 panic!("Expected distance constraint");
11076 };
11077 assert_eq!(distance.label_position, Some(label_position));
11078
11079 mock_ctx.close().await;
11080 }
11081
11082 #[tokio::test(flavor = "multi_thread")]
11083 async fn test_edit_distance_constraint_label_position() {
11084 let initial_source = "\
11085sketch(on = XY) {
11086 point(at = [var 1, var 2])
11087 point(at = [var 3, var 2])
11088}
11089";
11090
11091 let program = Program::parse(initial_source).unwrap().0.unwrap();
11092
11093 let mut frontend = FrontendState::new();
11094
11095 let mock_ctx = ExecutorContext::new_mock(None).await;
11096 let version = Version(0);
11097
11098 frontend.program = program.clone();
11099 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11100 frontend.update_state_after_exec(outcome, true);
11101 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11102 let sketch_id = sketch_object.id;
11103 let sketch = expect_sketch(sketch_object);
11104 let point0_id = *sketch.segments.first().unwrap();
11105 let point1_id = *sketch.segments.get(1).unwrap();
11106
11107 let constraint = Constraint::Distance(Distance {
11108 segments: vec![point0_id.into(), point1_id.into()],
11109 distance: Number {
11110 value: 2.0,
11111 units: NumericSuffix::Mm,
11112 },
11113 label_position: None,
11114 source: Default::default(),
11115 });
11116 let (_, scene_delta) = frontend
11117 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11118 .await
11119 .unwrap();
11120 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
11121 let sketch = expect_sketch(sketch_object);
11122 let constraint_id = sketch.constraints[0];
11123 let label_position = Point2d {
11124 x: Number {
11125 value: 10.0,
11126 units: NumericSuffix::Mm,
11127 },
11128 y: Number {
11129 value: 11.0,
11130 units: NumericSuffix::Mm,
11131 },
11132 };
11133
11134 let (src_delta, scene_delta) = frontend
11135 .edit_distance_constraint_label_position(
11136 &mock_ctx,
11137 version,
11138 sketch_id,
11139 constraint_id,
11140 label_position.clone(),
11141 vec![],
11142 )
11143 .await
11144 .unwrap();
11145 insta::assert_snapshot!("test_edit_distance_constraint_label_position", src_delta.text.as_str());
11146
11147 let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
11148 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11149 panic!("Expected constraint object");
11150 };
11151 let Constraint::Distance(distance) = constraint else {
11152 panic!("Expected distance constraint");
11153 };
11154 assert_eq!(distance.label_position, Some(label_position));
11155
11156 mock_ctx.close().await;
11157 }
11158
11159 #[tokio::test(flavor = "multi_thread")]
11160 async fn test_edit_distance_constraint_type_and_value() {
11161 let initial_source = "\
11162sketch(on = XY) {
11163 line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 3mm])
11164 distance([line1.start, line1.end]) == 5mm
11165}
11166";
11167
11168 let program = Program::parse(initial_source).unwrap().0.unwrap();
11169 let mut frontend = FrontendState::new();
11170 let mock_ctx = ExecutorContext::new_mock(None).await;
11171 let version = Version(0);
11172
11173 frontend.program = program.clone();
11174 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11175 frontend.update_state_after_exec(outcome, true);
11176 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11177 let sketch_id = sketch_object.id;
11178 let sketch = expect_sketch(sketch_object);
11179 let constraint_id = sketch.constraints[0];
11180 let point0_id = sketch.segments[0];
11181 let point1_id = sketch.segments[1];
11182 let label_position = Point2d {
11183 x: Number {
11184 value: 2.0,
11185 units: NumericSuffix::Mm,
11186 },
11187 y: Number {
11188 value: 5.0,
11189 units: NumericSuffix::Mm,
11190 },
11191 };
11192
11193 let (source_delta, scene_delta) = frontend
11194 .edit_distance_constraint_with_options(
11195 &mock_ctx,
11196 version,
11197 sketch_id,
11198 constraint_id,
11199 Constraint::HorizontalDistance(Distance {
11200 segments: vec![point0_id.into(), point1_id.into()],
11201 distance: Number {
11202 value: 4.0,
11203 units: NumericSuffix::Mm,
11204 },
11205 label_position: Some(label_position.clone()),
11206 source: Default::default(),
11207 }),
11208 EditConstraintOptions {
11209 commit_solved_initial_guesses: false,
11210 },
11211 )
11212 .await
11213 .unwrap();
11214 assert_eq!(
11215 source_delta.text,
11216 "\
11217sketch(on = XY) {
11218 line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 3mm])
11219 horizontalDistance([line1.start, line1.end], labelPosition = [2mm, 5mm]) == 4mm
11220}
11221"
11222 );
11223
11224 let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
11225 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11226 panic!("Expected constraint object");
11227 };
11228 let Constraint::HorizontalDistance(distance) = constraint else {
11229 panic!("Expected horizontal distance constraint");
11230 };
11231 assert_eq!(distance.distance.value, 4.0);
11232 assert_eq!(distance.label_position, Some(label_position));
11233
11234 mock_ctx.close().await;
11235 }
11236
11237 #[tokio::test(flavor = "multi_thread")]
11238 async fn test_edit_angle_constraint_label_position() {
11239 let initial_source = "\
11240sketch(on = XY) {
11241 line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11242 line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
11243 angle([line1, line2]) == 60deg
11244}
11245";
11246
11247 let program = Program::parse(initial_source).unwrap().0.unwrap();
11248 let mut frontend = FrontendState::new();
11249 let mock_ctx = ExecutorContext::new_mock(None).await;
11250 let version = Version(0);
11251
11252 frontend.program = program.clone();
11253 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11254 frontend.update_state_after_exec(outcome, true);
11255 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11256 let sketch_id = sketch_object.id;
11257 let sketch = expect_sketch(sketch_object);
11258 let constraint_id = sketch.constraints[0];
11259 let label_position = Point2d {
11260 x: Number {
11261 value: 10.0,
11262 units: NumericSuffix::Mm,
11263 },
11264 y: Number {
11265 value: 11.0,
11266 units: NumericSuffix::Mm,
11267 },
11268 };
11269
11270 let (src_delta, scene_delta) = frontend
11271 .edit_distance_constraint_label_position(
11272 &mock_ctx,
11273 version,
11274 sketch_id,
11275 constraint_id,
11276 label_position.clone(),
11277 vec![],
11278 )
11279 .await
11280 .unwrap();
11281 assert_eq!(
11282 src_delta.text.as_str(),
11283 "\
11284sketch(on = XY) {
11285 line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11286 line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.46mm])
11287 angle([line1, line2], labelPosition = [10mm, 11mm]) == 60deg
11288}
11289"
11290 );
11291
11292 let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
11293 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11294 panic!("Expected constraint object");
11295 };
11296 let Constraint::Angle(angle) = constraint else {
11297 panic!("Expected angle constraint");
11298 };
11299 assert_eq!(angle.label_position, Some(label_position));
11300
11301 mock_ctx.close().await;
11302 }
11303
11304 #[tokio::test(flavor = "multi_thread")]
11305 async fn test_edit_angle_constraint_label_position_with_call_on_right() {
11306 let initial_source = "\
11307sketch(on = XY) {
11308 line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11309 line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
11310 60deg == angleDimension(lines = [line1, line2], sector = 1)
11311}
11312";
11313
11314 let program = Program::parse(initial_source).unwrap().0.unwrap();
11315 let mut frontend = FrontendState::new();
11316 let mock_ctx = ExecutorContext::new_mock(None).await;
11317 let version = Version(0);
11318
11319 frontend.program = program.clone();
11320 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11321 frontend.update_state_after_exec(outcome, true);
11322 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11323 let sketch_id = sketch_object.id;
11324 let sketch = expect_sketch(sketch_object);
11325 let constraint_id = sketch.constraints[0];
11326 let label_position = Point2d {
11327 x: Number {
11328 value: 10.0,
11329 units: NumericSuffix::Mm,
11330 },
11331 y: Number {
11332 value: 11.0,
11333 units: NumericSuffix::Mm,
11334 },
11335 };
11336
11337 let (src_delta, scene_delta) = frontend
11338 .edit_distance_constraint_label_position(
11339 &mock_ctx,
11340 version,
11341 sketch_id,
11342 constraint_id,
11343 label_position.clone(),
11344 vec![],
11345 )
11346 .await
11347 .unwrap();
11348 assert_eq!(
11349 src_delta.text.as_str(),
11350 "\
11351sketch(on = XY) {
11352 line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11353 line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.46mm])
11354 60deg == angleDimension(lines = [line1, line2], sector = 1, labelPosition = [10mm, 11mm])
11355}
11356"
11357 );
11358
11359 let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
11360 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11361 panic!("Expected constraint object");
11362 };
11363 let Constraint::Angle(angle) = constraint else {
11364 panic!("Expected angle constraint");
11365 };
11366 assert_eq!(angle.label_position, Some(label_position));
11367
11368 mock_ctx.close().await;
11369 }
11370
11371 #[tokio::test(flavor = "multi_thread")]
11372 async fn test_edit_angle_constraint() {
11373 let initial_source = "\
11374sketch(on = XY) {
11375 line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11376 line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
11377 angle([line1, line2]) == 60deg
11378}
11379";
11380
11381 let program = Program::parse(initial_source).unwrap().0.unwrap();
11382 let mut frontend = FrontendState::new();
11383 let mock_ctx = ExecutorContext::new_mock(None).await;
11384 let version = Version(0);
11385
11386 frontend.program = program.clone();
11387 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11388 frontend.update_state_after_exec(outcome, true);
11389 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11390 let sketch_id = sketch_object.id;
11391 let sketch = expect_sketch(sketch_object);
11392 let constraint_id = sketch.constraints[0];
11393 let line1_id = *sketch.segments.get(2).unwrap();
11394 let line2_id = *sketch.segments.get(5).unwrap();
11395 let label_position = Point2d {
11396 x: Number {
11397 value: 10.0,
11398 units: NumericSuffix::Mm,
11399 },
11400 y: Number {
11401 value: 11.0,
11402 units: NumericSuffix::Mm,
11403 },
11404 };
11405
11406 let (src_delta, scene_delta) = frontend
11407 .edit_angle_constraint_with_options(
11408 &mock_ctx,
11409 version,
11410 sketch_id,
11411 constraint_id,
11412 Angle {
11413 lines: vec![line2_id, line1_id],
11414 angle: Number {
11415 value: 60.0,
11416 units: NumericSuffix::Deg,
11417 },
11418 sector: Some(3),
11419 inverse: Some(false),
11420 label_position: Some(label_position.clone()),
11421 source: Default::default(),
11422 },
11423 EditConstraintOptions {
11424 commit_solved_initial_guesses: false,
11425 },
11426 )
11427 .await
11428 .unwrap();
11429 assert_eq!(
11430 src_delta.text.as_str(),
11431 "\
11432sketch(on = XY) {
11433 line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11434 line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
11435 angleDimension(lines = [line2, line1], sector = 3, labelPosition = [10mm, 11mm]) == 60deg
11436}
11437"
11438 );
11439
11440 let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
11441 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11442 panic!("Expected constraint object");
11443 };
11444 let Constraint::Angle(angle) = constraint else {
11445 panic!("Expected angle constraint");
11446 };
11447 assert_eq!(angle.lines, vec![line2_id, line1_id]);
11448 assert_eq!(angle.sector, Some(3));
11449 assert_eq!(angle.inverse, Some(false));
11450 assert_eq!(angle.label_position, Some(label_position));
11451
11452 mock_ctx.close().await;
11453 }
11454
11455 #[tokio::test(flavor = "multi_thread")]
11456 async fn test_edit_angle_constraint_with_call_on_right() {
11457 let initial_source = "\
11458sketch(on = XY) {
11459 line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11460 line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
11461 60deg == angle([line1, line2])
11462}
11463";
11464
11465 let program = Program::parse(initial_source).unwrap().0.unwrap();
11466 let mut frontend = FrontendState::new();
11467 let mock_ctx = ExecutorContext::new_mock(None).await;
11468 let version = Version(0);
11469
11470 frontend.program = program.clone();
11471 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11472 frontend.update_state_after_exec(outcome, true);
11473 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11474 let sketch_id = sketch_object.id;
11475 let sketch = expect_sketch(sketch_object);
11476 let constraint_id = sketch.constraints[0];
11477 let line1_id = *sketch.segments.get(2).unwrap();
11478 let line2_id = *sketch.segments.get(5).unwrap();
11479
11480 let (src_delta, _) = frontend
11481 .edit_angle_constraint_with_options(
11482 &mock_ctx,
11483 version,
11484 sketch_id,
11485 constraint_id,
11486 Angle {
11487 lines: vec![line2_id, line1_id],
11488 angle: Number {
11489 value: 60.0,
11490 units: NumericSuffix::Deg,
11491 },
11492 sector: Some(3),
11493 inverse: Some(false),
11494 label_position: None,
11495 source: Default::default(),
11496 },
11497 EditConstraintOptions {
11498 commit_solved_initial_guesses: false,
11499 },
11500 )
11501 .await
11502 .unwrap();
11503 assert_eq!(
11504 src_delta.text.as_str(),
11505 "\
11506sketch(on = XY) {
11507 line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11508 line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
11509 60deg == angleDimension(lines = [line2, line1], sector = 3)
11510}
11511"
11512 );
11513
11514 mock_ctx.close().await;
11515 }
11516
11517 #[tokio::test(flavor = "multi_thread")]
11518 async fn test_edit_segments_can_commit_constraint_label_position_in_same_execution() {
11519 let initial_source = "\
11520@settings(kclVersion = 2.0)
11521
11522sketch001 = sketch(on = XZ) {
11523 line1 = line(start = [var 0mm, var 12.55mm], end = [var -6.03mm, var 8.51mm])
11524 line3 = line(start = [var -7.41mm, var 2.92mm], end = [var -1.47mm, var 4.32mm])
11525 distance([line1.start, line3.end], labelPosition = [5.56mm, 8.65mm]) == 8.36mm
11526 vertical([line1.start, ORIGIN])
11527}
11528";
11529
11530 let program = Program::parse(initial_source).unwrap().0.unwrap();
11531 let mut frontend = FrontendState::new();
11532 let mock_ctx = ExecutorContext::new_mock(None).await;
11533 let version = Version(0);
11534
11535 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
11536 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11537 let sketch_id = sketch_object.id;
11538 let sketch = expect_sketch(sketch_object);
11539 let constraint_id = sketch
11540 .constraints
11541 .iter()
11542 .copied()
11543 .find(|constraint_id| {
11544 matches!(
11545 frontend.scene_graph.objects[constraint_id.0].kind,
11546 ObjectKind::Constraint {
11547 constraint: Constraint::Distance(_)
11548 }
11549 )
11550 })
11551 .unwrap();
11552 let line1_id = sketch
11553 .segments
11554 .iter()
11555 .copied()
11556 .find(|segment_id| {
11557 matches!(
11558 frontend.scene_graph.objects[segment_id.0].kind,
11559 ObjectKind::Segment {
11560 segment: Segment::Line(_)
11561 }
11562 )
11563 })
11564 .unwrap();
11565 let label_position = Point2d {
11566 x: Number {
11567 value: 7.0,
11568 units: NumericSuffix::Mm,
11569 },
11570 y: Number {
11571 value: 9.0,
11572 units: NumericSuffix::Mm,
11573 },
11574 };
11575
11576 let (source_delta, scene_delta) = frontend
11577 .edit_segments_with_options(
11578 &mock_ctx,
11579 version,
11580 sketch_id,
11581 vec![ExistingSegmentCtor {
11582 id: line1_id,
11583 ctor: SegmentCtor::Line(LineCtor {
11584 start: point_expr_mm(2.0, 15.55),
11585 end: point_expr_mm(-4.03, 11.51),
11586 construction: None,
11587 }),
11588 }],
11589 EditSegmentsOptions {
11590 anchor_segment_ids: Some(vec![]),
11591 drag_anchors: vec![SegmentDragAnchor {
11592 segment_id: line1_id,
11593 target: label_position.clone(),
11594 }],
11595 constraint_label_edits: vec![ConstraintLabelPositionEdit {
11596 constraint_id,
11597 label_position: label_position.clone(),
11598 }],
11599 commit_solved_initial_guesses: true,
11600 },
11601 )
11602 .await
11603 .unwrap();
11604
11605 assert!(source_delta.text.contains("labelPosition = [7mm, 9mm]"));
11606 let constraint_object = &scene_delta.new_graph.objects[constraint_id.0];
11607 let ObjectKind::Constraint {
11608 constraint: Constraint::Distance(distance),
11609 } = &constraint_object.kind
11610 else {
11611 panic!("Expected distance constraint object");
11612 };
11613 assert_eq!(distance.label_position, Some(label_position));
11614
11615 let snapped_label_position = Point2d {
11616 x: Number {
11617 value: 8.0,
11618 units: NumericSuffix::Mm,
11619 },
11620 y: Number {
11621 value: 10.0,
11622 units: NumericSuffix::Mm,
11623 },
11624 };
11625 let (source_delta, scene_delta) = frontend
11626 .edit_segments_with_options(
11627 &mock_ctx,
11628 version,
11629 sketch_id,
11630 vec![],
11631 EditSegmentsOptions {
11632 anchor_segment_ids: Some(vec![line1_id]),
11633 drag_anchors: vec![],
11634 constraint_label_edits: vec![ConstraintLabelPositionEdit {
11635 constraint_id,
11636 label_position: snapped_label_position.clone(),
11637 }],
11638 commit_solved_initial_guesses: true,
11639 },
11640 )
11641 .await
11642 .unwrap();
11643
11644 assert!(source_delta.text.contains("labelPosition = [8mm, 10mm]"));
11645 let constraint_object = &scene_delta.new_graph.objects[constraint_id.0];
11646 let ObjectKind::Constraint {
11647 constraint: Constraint::Distance(distance),
11648 } = &constraint_object.kind
11649 else {
11650 panic!("Expected distance constraint object");
11651 };
11652 assert_eq!(distance.label_position, Some(snapped_label_position));
11653
11654 mock_ctx.close().await;
11655 }
11656
11657 #[tokio::test(flavor = "multi_thread")]
11658 async fn test_edit_distance_constraint_label_position_preserves_anchor_segment_solution() {
11659 let initial_source = "\
11660sketch(on = XY) {
11661 point1 = point(at = [var 0mm, var 0mm])
11662 point2 = point(at = [var 10mm, var 0mm])
11663 distance([point1, point2]) == 5mm
11664}
11665";
11666
11667 let program = Program::parse(initial_source).unwrap().0.unwrap();
11668 let mut frontend = FrontendState::new();
11669 let mock_ctx = ExecutorContext::new_mock(None).await;
11670 let version = Version(0);
11671
11672 frontend.program = program.clone();
11673 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11674 frontend.update_state_after_exec(outcome, true);
11675 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11676 let sketch_id = sketch_object.id;
11677 let sketch = expect_sketch(sketch_object);
11678 let point0_id = sketch.segments[0];
11679 let point1_id = sketch.segments[1];
11680 let constraint_id = sketch.constraints[0];
11681
11682 let edited_segments = vec![ExistingSegmentCtor {
11683 id: point0_id,
11684 ctor: SegmentCtor::Point(PointCtor {
11685 position: Point2d {
11686 x: Expr::Var(Number {
11687 value: 2.0,
11688 units: NumericSuffix::Mm,
11689 }),
11690 y: Expr::Var(Number {
11691 value: 1.0,
11692 units: NumericSuffix::Mm,
11693 }),
11694 },
11695 }),
11696 }];
11697 let (_, scene_delta) = frontend
11698 .edit_segments(&mock_ctx, version, sketch_id, edited_segments)
11699 .await
11700 .unwrap();
11701 let point0_after_segment_edit = point_position(&scene_delta.new_graph, point0_id);
11702 let point1_after_segment_edit = point_position(&scene_delta.new_graph, point1_id);
11703
11704 let label_position = Point2d {
11705 x: Number {
11706 value: 3.0,
11707 units: NumericSuffix::Mm,
11708 },
11709 y: Number {
11710 value: 4.0,
11711 units: NumericSuffix::Mm,
11712 },
11713 };
11714 let (_, scene_delta) = frontend
11715 .edit_distance_constraint_label_position(
11716 &mock_ctx,
11717 version,
11718 sketch_id,
11719 constraint_id,
11720 label_position,
11721 vec![point0_id],
11722 )
11723 .await
11724 .unwrap();
11725
11726 assert_point_position_close(
11727 point_position(&scene_delta.new_graph, point0_id),
11728 point0_after_segment_edit,
11729 );
11730 assert_point_position_close(
11731 point_position(&scene_delta.new_graph, point1_id),
11732 point1_after_segment_edit,
11733 );
11734
11735 mock_ctx.close().await;
11736 }
11737
11738 #[tokio::test(flavor = "multi_thread")]
11739 async fn test_distance_point_line() {
11740 let initial_source = "\
11741sketch(on = XY) {
11742 point(at = [var 0, var 5])
11743 line(start = [var 0, var 0], end = [var 10, var 0])
11744}
11745";
11746
11747 let program = Program::parse(initial_source).unwrap().0.unwrap();
11748
11749 let mut frontend = FrontendState::new();
11750
11751 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
11752 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
11753 let version = Version(0);
11754
11755 let outcome = frontend.hack_set_program(&ctx, program).await.unwrap();
11756 assert!(matches!(outcome, SetProgramOutcome::Success { .. }), "{outcome:?}");
11757 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11758 let sketch_id = sketch_object.id;
11759 let sketch = expect_sketch(sketch_object);
11760 let point_id = *sketch.segments.first().unwrap();
11761 let line_id = *sketch
11762 .segments
11763 .iter()
11764 .find(|segment_id| {
11765 matches!(
11766 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11767 Some(ObjectKind::Segment {
11768 segment: Segment::Line(_)
11769 })
11770 )
11771 })
11772 .unwrap();
11773
11774 let label_position = Point2d {
11775 x: Number {
11776 value: 10.0,
11777 units: NumericSuffix::Mm,
11778 },
11779 y: Number {
11780 value: 11.0,
11781 units: NumericSuffix::Mm,
11782 },
11783 };
11784 let constraint = Constraint::Distance(Distance {
11785 segments: vec![point_id.into(), line_id.into()],
11786 distance: Number {
11787 value: 5.0,
11788 units: NumericSuffix::Mm,
11789 },
11790 label_position: Some(label_position.clone()),
11791 source: Default::default(),
11792 });
11793 let (src_delta, scene_delta) = frontend
11794 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11795 .await
11796 .unwrap();
11797 insta::assert_snapshot!("test_distance_point_line", src_delta.text.as_str());
11798 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
11799 let sketch = expect_sketch(sketch_object);
11800 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
11801 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11802 panic!("Expected constraint object");
11803 };
11804 let Constraint::Distance(distance) = constraint else {
11805 panic!("Expected distance constraint");
11806 };
11807 assert_eq!(distance.label_position, Some(label_position));
11808
11809 ctx.close().await;
11810 mock_ctx.close().await;
11811 }
11812
11813 #[tokio::test(flavor = "multi_thread")]
11814 async fn test_distance_point_arc() {
11815 let initial_source = "\
11816sketch(on = XY) {
11817 point(at = [var 0, var 8])
11818 arc(start = [var 5, var 0], end = [var 0, var 5], center = [var 0, var 0])
11819}
11820";
11821
11822 let program = Program::parse(initial_source).unwrap().0.unwrap();
11823
11824 let mut frontend = FrontendState::new();
11825
11826 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
11827 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
11828 let version = Version(0);
11829
11830 frontend.hack_set_program(&ctx, program).await.unwrap();
11831 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11832 let sketch_id = sketch_object.id;
11833 let sketch = expect_sketch(sketch_object);
11834 let point_id = *sketch.segments.first().unwrap();
11835 let arc_id = *sketch
11836 .segments
11837 .iter()
11838 .find(|segment_id| {
11839 matches!(
11840 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11841 Some(ObjectKind::Segment {
11842 segment: Segment::Arc(_)
11843 })
11844 )
11845 })
11846 .unwrap();
11847
11848 let constraint = Constraint::Distance(Distance {
11849 segments: vec![point_id.into(), arc_id.into()],
11850 distance: Number {
11851 value: 3.0,
11852 units: NumericSuffix::Mm,
11853 },
11854 label_position: None,
11855 source: Default::default(),
11856 });
11857 let (src_delta, _scene_delta) = frontend
11858 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11859 .await
11860 .unwrap();
11861 insta::assert_snapshot!("test_distance_point_arc", src_delta.text.as_str());
11862
11863 ctx.close().await;
11864 mock_ctx.close().await;
11865 }
11866
11867 #[tokio::test(flavor = "multi_thread")]
11868 async fn test_distance_arc_origin() {
11869 let initial_source = "\
11870sketch001 = sketch(on = XY) {
11871 arc(start = [var -4.13mm, var -0.59mm], end = [var -3.47mm, var 3.38mm], center = [var -4.55mm, var 1.52mm])
11872}
11873";
11874
11875 let program = Program::parse(initial_source).unwrap().0.unwrap();
11876
11877 let mut frontend = FrontendState::new();
11878
11879 let mock_ctx = ExecutorContext::new_mock(None).await;
11880 let version = Version(0);
11881
11882 frontend.program = program.clone();
11883 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11884 frontend.update_state_after_exec(outcome, true);
11885 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11886 let sketch_id = sketch_object.id;
11887 let sketch = expect_sketch(sketch_object);
11888 let arc_id = *sketch
11889 .segments
11890 .iter()
11891 .find(|segment_id| {
11892 matches!(
11893 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11894 Some(ObjectKind::Segment {
11895 segment: Segment::Arc(_)
11896 })
11897 )
11898 })
11899 .unwrap();
11900
11901 let constraint = Constraint::Distance(Distance {
11902 segments: vec![arc_id.into(), ConstraintSegment::ORIGIN],
11903 distance: Number {
11904 value: 3.0,
11905 units: NumericSuffix::Mm,
11906 },
11907 label_position: None,
11908 source: Default::default(),
11909 });
11910 let (src_delta, _scene_delta) = frontend
11911 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11912 .await
11913 .unwrap();
11914 insta::assert_snapshot!("test_distance_arc_origin", src_delta.text.as_str());
11915
11916 mock_ctx.close().await;
11917 }
11918
11919 #[tokio::test(flavor = "multi_thread")]
11920 async fn test_distance_line_origin() {
11921 let initial_source = "\
11922sketch(on = XY) {
11923 line(start = [var 5, var 0], end = [var 5, var 10])
11924}
11925";
11926
11927 let program = Program::parse(initial_source).unwrap().0.unwrap();
11928
11929 let mut frontend = FrontendState::new();
11930
11931 let mock_ctx = ExecutorContext::new_mock(None).await;
11932 let version = Version(0);
11933
11934 frontend.program = program.clone();
11935 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11936 frontend.update_state_after_exec(outcome, true);
11937 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11938 let sketch_id = sketch_object.id;
11939 let sketch = expect_sketch(sketch_object);
11940 let line_id = *sketch
11941 .segments
11942 .iter()
11943 .find(|segment_id| {
11944 matches!(
11945 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11946 Some(ObjectKind::Segment {
11947 segment: Segment::Line(_)
11948 })
11949 )
11950 })
11951 .unwrap();
11952
11953 let constraint = Constraint::Distance(Distance {
11954 segments: vec![ConstraintSegment::ORIGIN, line_id.into()],
11955 distance: Number {
11956 value: 5.0,
11957 units: NumericSuffix::Mm,
11958 },
11959 label_position: None,
11960 source: Default::default(),
11961 });
11962 let (src_delta, _scene_delta) = frontend
11963 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11964 .await
11965 .unwrap();
11966 insta::assert_snapshot!("test_distance_line_origin", src_delta.text.as_str());
11967
11968 mock_ctx.close().await;
11969 }
11970
11971 #[tokio::test(flavor = "multi_thread")]
11972 async fn test_distance_line_circle() {
11973 let initial_source = "\
11974sketch(on = XY) {
11975 line(start = [var -10, var 8], end = [var 10, var 8])
11976 circle(start = [var 5, var 0], center = [var 0, var 0])
11977}
11978";
11979
11980 let program = Program::parse(initial_source).unwrap().0.unwrap();
11981
11982 let mut frontend = FrontendState::new();
11983
11984 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
11985 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
11986 let version = Version(0);
11987
11988 frontend.hack_set_program(&ctx, program).await.unwrap();
11989 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11990 let sketch_id = sketch_object.id;
11991 let sketch = expect_sketch(sketch_object);
11992 let line_id = *sketch
11993 .segments
11994 .iter()
11995 .find(|segment_id| {
11996 matches!(
11997 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11998 Some(ObjectKind::Segment {
11999 segment: Segment::Line(_)
12000 })
12001 )
12002 })
12003 .unwrap();
12004 let circle_id = *sketch
12005 .segments
12006 .iter()
12007 .find(|segment_id| {
12008 matches!(
12009 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
12010 Some(ObjectKind::Segment {
12011 segment: Segment::Circle(_)
12012 })
12013 )
12014 })
12015 .unwrap();
12016
12017 let constraint = Constraint::Distance(Distance {
12018 segments: vec![line_id.into(), circle_id.into()],
12019 distance: Number {
12020 value: 3.0,
12021 units: NumericSuffix::Mm,
12022 },
12023 label_position: None,
12024 source: Default::default(),
12025 });
12026 let (src_delta, _scene_delta) = frontend
12027 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12028 .await
12029 .unwrap();
12030 insta::assert_snapshot!("test_distance_line_circle", src_delta.text.as_str());
12031
12032 ctx.close().await;
12033 mock_ctx.close().await;
12034 }
12035
12036 #[tokio::test(flavor = "multi_thread")]
12037 async fn test_distance_circle_arc() {
12038 let initial_source = "\
12039sketch(on = XY) {
12040 circle(start = [var 5, var 0], center = [var 0, var 0])
12041 arc(start = [var 15, var 0], end = [var 10, var 5], center = [var 10, var 0])
12042}
12043";
12044
12045 let program = Program::parse(initial_source).unwrap().0.unwrap();
12046
12047 let mut frontend = FrontendState::new();
12048
12049 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
12050 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
12051 let version = Version(0);
12052
12053 let outcome = frontend.hack_set_program(&ctx, program).await.unwrap();
12054 assert!(matches!(outcome, SetProgramOutcome::Success { .. }), "{outcome:?}");
12055 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12056 let sketch_id = sketch_object.id;
12057 let sketch = expect_sketch(sketch_object);
12058 let circle_id = *sketch
12059 .segments
12060 .iter()
12061 .find(|segment_id| {
12062 matches!(
12063 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
12064 Some(ObjectKind::Segment {
12065 segment: Segment::Circle(_)
12066 })
12067 )
12068 })
12069 .unwrap();
12070 let arc_id = *sketch
12071 .segments
12072 .iter()
12073 .find(|segment_id| {
12074 matches!(
12075 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
12076 Some(ObjectKind::Segment {
12077 segment: Segment::Arc(_)
12078 })
12079 )
12080 })
12081 .unwrap();
12082
12083 let constraint = Constraint::Distance(Distance {
12084 segments: vec![circle_id.into(), arc_id.into()],
12085 distance: Number {
12086 value: 3.0,
12087 units: NumericSuffix::Mm,
12088 },
12089 label_position: None,
12090 source: Default::default(),
12091 });
12092 let (src_delta, _scene_delta) = frontend
12093 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12094 .await
12095 .unwrap();
12096 insta::assert_snapshot!("test_distance_circle_arc", src_delta.text.as_str());
12097
12098 ctx.close().await;
12099 mock_ctx.close().await;
12100 }
12101
12102 #[tokio::test(flavor = "multi_thread")]
12103 async fn test_distance_parallel_lines() {
12104 let initial_source = "\
12105sketch(on = XY) {
12106 line(start = [var 0, var 0], end = [var 10, var 0])
12107 line(start = [var 0, var 5], end = [var 10, var 5])
12108}
12109";
12110
12111 let program = Program::parse(initial_source).unwrap().0.unwrap();
12112
12113 let mut frontend = FrontendState::new();
12114
12115 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
12116 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
12117 let version = Version(0);
12118
12119 frontend.hack_set_program(&ctx, program).await.unwrap();
12120 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12121 let sketch_id = sketch_object.id;
12122 let sketch = expect_sketch(sketch_object);
12123 let line_ids = sketch
12124 .segments
12125 .iter()
12126 .copied()
12127 .filter(|segment_id| {
12128 matches!(
12129 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
12130 Some(ObjectKind::Segment {
12131 segment: Segment::Line(_)
12132 })
12133 )
12134 })
12135 .collect::<Vec<_>>();
12136
12137 let constraint = Constraint::Distance(Distance {
12138 segments: vec![line_ids[0].into(), line_ids[1].into()],
12139 distance: Number {
12140 value: 5.0,
12141 units: NumericSuffix::Mm,
12142 },
12143 label_position: None,
12144 source: Default::default(),
12145 });
12146 let (src_delta, _scene_delta) = frontend
12147 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12148 .await
12149 .unwrap();
12150 insta::assert_snapshot!("test_distance_parallel_lines", src_delta.text.as_str());
12151
12152 ctx.close().await;
12153 mock_ctx.close().await;
12154 }
12155
12156 #[tokio::test(flavor = "multi_thread")]
12157 async fn test_distance_non_parallel_lines_lowers_to_distance() {
12158 let initial_source = "\
12163sketch(on = XY) {
12164 line(start = [var 0, var 0], end = [var 10, var 0])
12165 line(start = [var 0, var 0], end = [var 10, var 10])
12166}
12167";
12168
12169 let program = Program::parse(initial_source).unwrap().0.unwrap();
12170
12171 let mut frontend = FrontendState::new();
12172
12173 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
12174 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
12175 let version = Version(0);
12176
12177 frontend.hack_set_program(&ctx, program).await.unwrap();
12178 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12179 let sketch_id = sketch_object.id;
12180 let sketch = expect_sketch(sketch_object);
12181 let line_ids = sketch
12182 .segments
12183 .iter()
12184 .copied()
12185 .filter(|segment_id| {
12186 matches!(
12187 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
12188 Some(ObjectKind::Segment {
12189 segment: Segment::Line(_)
12190 })
12191 )
12192 })
12193 .collect::<Vec<_>>();
12194
12195 let constraint = Constraint::Distance(Distance {
12196 segments: vec![line_ids[0].into(), line_ids[1].into()],
12197 distance: Number {
12198 value: 5.0,
12199 units: NumericSuffix::Mm,
12200 },
12201 label_position: None,
12202 source: Default::default(),
12203 });
12204 let (src_delta, _scene_delta) = frontend
12205 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12206 .await
12207 .unwrap();
12208 insta::assert_snapshot!(
12209 "test_distance_non_parallel_lines_lowers_to_distance",
12210 src_delta.text.as_str()
12211 );
12212
12213 ctx.close().await;
12214 mock_ctx.close().await;
12215 }
12216
12217 #[tokio::test(flavor = "multi_thread")]
12218 async fn test_horizontal_distance_two_points() {
12219 let initial_source = "\
12220sketch(on = XY) {
12221 point(at = [var 1, var 2])
12222 point(at = [var 3, var 4])
12223}
12224";
12225
12226 let program = Program::parse(initial_source).unwrap().0.unwrap();
12227
12228 let mut frontend = FrontendState::new();
12229
12230 let mock_ctx = ExecutorContext::new_mock(None).await;
12231 let version = Version(0);
12232
12233 frontend.program = program.clone();
12234 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12235 frontend.update_state_after_exec(outcome, true);
12236 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12237 let sketch_id = sketch_object.id;
12238 let sketch = expect_sketch(sketch_object);
12239 let point0_id = *sketch.segments.first().unwrap();
12240 let point1_id = *sketch.segments.get(1).unwrap();
12241 let label_position = Point2d {
12242 x: Number {
12243 value: 10.0,
12244 units: NumericSuffix::Mm,
12245 },
12246 y: Number {
12247 value: 11.0,
12248 units: NumericSuffix::Mm,
12249 },
12250 };
12251
12252 let constraint = Constraint::HorizontalDistance(Distance {
12253 segments: vec![point0_id.into(), point1_id.into()],
12254 distance: Number {
12255 value: 2.0,
12256 units: NumericSuffix::Mm,
12257 },
12258 label_position: Some(label_position.clone()),
12259 source: Default::default(),
12260 });
12261 let (src_delta, scene_delta) = frontend
12262 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12263 .await
12264 .unwrap();
12265 insta::assert_snapshot!("test_horizontal_distance_two_points", src_delta.text.as_str());
12266 assert_eq!(
12267 scene_delta.new_graph.objects.len(),
12268 5,
12269 "{:#?}",
12270 scene_delta.new_graph.objects
12271 );
12272 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
12273 let sketch = expect_sketch(sketch_object);
12274 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
12275 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12276 panic!("Expected constraint object");
12277 };
12278 let Constraint::HorizontalDistance(distance) = constraint else {
12279 panic!("Expected horizontal distance constraint");
12280 };
12281 assert_eq!(distance.label_position, Some(label_position));
12282
12283 mock_ctx.close().await;
12284 }
12285
12286 #[tokio::test(flavor = "multi_thread")]
12287 async fn test_radius_single_arc_segment() {
12288 let initial_source = "\
12289sketch(on = XY) {
12290 arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
12291}
12292";
12293
12294 let program = Program::parse(initial_source).unwrap().0.unwrap();
12295
12296 let mut frontend = FrontendState::new();
12297
12298 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
12299 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
12300 let version = Version(0);
12301
12302 frontend.hack_set_program(&ctx, program).await.unwrap();
12303 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12304 let sketch_id = sketch_object.id;
12305 let sketch = expect_sketch(sketch_object);
12306 let arc_id = sketch
12308 .segments
12309 .iter()
12310 .find(|&seg_id| {
12311 let obj = frontend.scene_graph.objects.get(seg_id.0);
12312 matches!(
12313 obj.map(|o| &o.kind),
12314 Some(ObjectKind::Segment {
12315 segment: Segment::Arc(_)
12316 })
12317 )
12318 })
12319 .unwrap();
12320
12321 let constraint = Constraint::Radius(Radius {
12322 arc: *arc_id,
12323 radius: Number {
12324 value: 5.0,
12325 units: NumericSuffix::Mm,
12326 },
12327 label_position: None,
12328 source: Default::default(),
12329 });
12330 let (src_delta, scene_delta) = frontend
12331 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12332 .await
12333 .unwrap();
12334 insta::assert_snapshot!("test_radius_single_arc_segment", src_delta.text.as_str());
12335 assert_eq!(
12336 scene_delta.new_graph.objects.len(),
12337 7, "{:#?}",
12339 scene_delta.new_graph.objects
12340 );
12341
12342 ctx.close().await;
12343 mock_ctx.close().await;
12344 }
12345
12346 #[tokio::test(flavor = "multi_thread")]
12347 async fn test_radius_single_arc_segment_with_label_position() {
12348 let initial_source = "\
12349sketch(on = XY) {
12350 arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
12351}
12352";
12353
12354 let program = Program::parse(initial_source).unwrap().0.unwrap();
12355 let mut frontend = FrontendState::new();
12356 let mock_ctx = ExecutorContext::new_mock(None).await;
12357 let version = Version(0);
12358
12359 frontend.program = program.clone();
12360 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12361 frontend.update_state_after_exec(outcome, true);
12362 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12363 let sketch_id = sketch_object.id;
12364 let sketch = expect_sketch(sketch_object);
12365 let arc_id = sketch
12366 .segments
12367 .iter()
12368 .find(|&seg_id| {
12369 let obj = frontend.scene_graph.objects.get(seg_id.0);
12370 matches!(
12371 obj.map(|o| &o.kind),
12372 Some(ObjectKind::Segment {
12373 segment: Segment::Arc(_)
12374 })
12375 )
12376 })
12377 .unwrap();
12378
12379 let label_position = Point2d {
12380 x: Number {
12381 value: 10.0,
12382 units: NumericSuffix::Mm,
12383 },
12384 y: Number {
12385 value: 11.0,
12386 units: NumericSuffix::Mm,
12387 },
12388 };
12389 let constraint = Constraint::Radius(Radius {
12390 arc: *arc_id,
12391 radius: Number {
12392 value: 5.0,
12393 units: NumericSuffix::Mm,
12394 },
12395 label_position: Some(label_position.clone()),
12396 source: Default::default(),
12397 });
12398 let (src_delta, scene_delta) = frontend
12399 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12400 .await
12401 .unwrap();
12402 insta::assert_snapshot!(
12403 "test_radius_single_arc_segment_with_label_position",
12404 src_delta.text.as_str()
12405 );
12406
12407 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
12408 let sketch = expect_sketch(sketch_object);
12409 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
12410 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12411 panic!("Expected constraint object");
12412 };
12413 let Constraint::Radius(radius) = constraint else {
12414 panic!("Expected radius constraint");
12415 };
12416 assert_eq!(radius.label_position, Some(label_position));
12417
12418 mock_ctx.close().await;
12419 }
12420
12421 #[tokio::test(flavor = "multi_thread")]
12422 async fn test_edit_radius_constraint_label_position() {
12423 let initial_source = "\
12424sketch(on = XY) {
12425 arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
12426 radius(arc1) == 5mm
12427}
12428";
12429
12430 let program = Program::parse(initial_source).unwrap().0.unwrap();
12431 let mut frontend = FrontendState::new();
12432 let mock_ctx = ExecutorContext::new_mock(None).await;
12433 let version = Version(0);
12434
12435 frontend.program = program.clone();
12436 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12437 frontend.update_state_after_exec(outcome, true);
12438 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12439 let sketch_id = sketch_object.id;
12440 let sketch = expect_sketch(sketch_object);
12441 let constraint_id = sketch.constraints[0];
12442 let label_position = Point2d {
12443 x: Number {
12444 value: 10.0,
12445 units: NumericSuffix::Mm,
12446 },
12447 y: Number {
12448 value: 11.0,
12449 units: NumericSuffix::Mm,
12450 },
12451 };
12452
12453 let (src_delta, scene_delta) = frontend
12454 .edit_distance_constraint_label_position(
12455 &mock_ctx,
12456 version,
12457 sketch_id,
12458 constraint_id,
12459 label_position.clone(),
12460 vec![],
12461 )
12462 .await
12463 .unwrap();
12464 insta::assert_snapshot!("test_edit_radius_constraint_label_position", src_delta.text.as_str());
12465
12466 let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
12467 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12468 panic!("Expected constraint object");
12469 };
12470 let Constraint::Radius(radius) = constraint else {
12471 panic!("Expected radius constraint");
12472 };
12473 assert_eq!(radius.label_position, Some(label_position));
12474
12475 mock_ctx.close().await;
12476 }
12477
12478 #[tokio::test(flavor = "multi_thread")]
12479 async fn test_vertical_distance_two_points() {
12480 let initial_source = "\
12481sketch(on = XY) {
12482 point(at = [var 1, var 2])
12483 point(at = [var 3, var 4])
12484}
12485";
12486
12487 let program = Program::parse(initial_source).unwrap().0.unwrap();
12488
12489 let mut frontend = FrontendState::new();
12490
12491 let mock_ctx = ExecutorContext::new_mock(None).await;
12492 let version = Version(0);
12493
12494 frontend.program = program.clone();
12495 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12496 frontend.update_state_after_exec(outcome, true);
12497 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12498 let sketch_id = sketch_object.id;
12499 let sketch = expect_sketch(sketch_object);
12500 let point0_id = *sketch.segments.first().unwrap();
12501 let point1_id = *sketch.segments.get(1).unwrap();
12502 let label_position = Point2d {
12503 x: Number {
12504 value: 10.0,
12505 units: NumericSuffix::Mm,
12506 },
12507 y: Number {
12508 value: 11.0,
12509 units: NumericSuffix::Mm,
12510 },
12511 };
12512
12513 let constraint = Constraint::VerticalDistance(Distance {
12514 segments: vec![point0_id.into(), point1_id.into()],
12515 distance: Number {
12516 value: 2.0,
12517 units: NumericSuffix::Mm,
12518 },
12519 label_position: Some(label_position.clone()),
12520 source: Default::default(),
12521 });
12522 let (src_delta, scene_delta) = frontend
12523 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12524 .await
12525 .unwrap();
12526 insta::assert_snapshot!("test_vertical_distance_two_points", src_delta.text.as_str());
12527 assert_eq!(
12528 scene_delta.new_graph.objects.len(),
12529 5,
12530 "{:#?}",
12531 scene_delta.new_graph.objects
12532 );
12533 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
12534 let sketch = expect_sketch(sketch_object);
12535 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
12536 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12537 panic!("Expected constraint object");
12538 };
12539 let Constraint::VerticalDistance(distance) = constraint else {
12540 panic!("Expected vertical distance constraint");
12541 };
12542 assert_eq!(distance.label_position, Some(label_position));
12543
12544 mock_ctx.close().await;
12545 }
12546
12547 #[tokio::test(flavor = "multi_thread")]
12548 async fn test_add_fixed_standalone_point() {
12549 let initial_source = "\
12550sketch(on = XY) {
12551 point(at = [var 1, var 2])
12552}
12553";
12554
12555 let program = Program::parse(initial_source).unwrap().0.unwrap();
12556
12557 let mut frontend = FrontendState::new();
12558
12559 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
12560 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
12561 let version = Version(0);
12562
12563 frontend.hack_set_program(&ctx, program).await.unwrap();
12564 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12565 let sketch_id = sketch_object.id;
12566 let sketch = expect_sketch(sketch_object);
12567 let point_id = *sketch.segments.first().unwrap();
12568
12569 let (src_delta, scene_delta) = frontend
12570 .add_constraint(
12571 &mock_ctx,
12572 version,
12573 sketch_id,
12574 Constraint::Fixed(Fixed {
12575 points: vec![FixedPoint {
12576 point: point_id,
12577 position: Point2d {
12578 x: Number {
12579 value: 2.0,
12580 units: NumericSuffix::Mm,
12581 },
12582 y: Number {
12583 value: 3.0,
12584 units: NumericSuffix::Mm,
12585 },
12586 },
12587 }],
12588 }),
12589 )
12590 .await
12591 .unwrap();
12592 insta::assert_snapshot!("test_add_fixed_standalone_point", src_delta.text.as_str());
12593 assert_eq!(
12594 scene_delta.new_graph.objects.len(),
12595 4,
12596 "{:#?}",
12597 scene_delta.new_graph.objects
12598 );
12599
12600 ctx.close().await;
12601 mock_ctx.close().await;
12602 }
12603
12604 #[tokio::test(flavor = "multi_thread")]
12605 async fn test_add_fixed_multiple_points() {
12606 let initial_source = "\
12607sketch(on = XY) {
12608 point(at = [var 1, var 2])
12609 point(at = [var 3, var 4])
12610}
12611";
12612
12613 let program = Program::parse(initial_source).unwrap().0.unwrap();
12614
12615 let mut frontend = FrontendState::new();
12616
12617 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
12618 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
12619 let version = Version(0);
12620
12621 frontend.hack_set_program(&ctx, program).await.unwrap();
12622 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12623 let sketch_id = sketch_object.id;
12624 let sketch = expect_sketch(sketch_object);
12625 let point0_id = *sketch.segments.first().unwrap();
12626 let point1_id = *sketch.segments.get(1).unwrap();
12627
12628 let (src_delta, scene_delta) = frontend
12629 .add_constraint(
12630 &mock_ctx,
12631 version,
12632 sketch_id,
12633 Constraint::Fixed(Fixed {
12634 points: vec![
12635 FixedPoint {
12636 point: point0_id,
12637 position: Point2d {
12638 x: Number {
12639 value: 2.0,
12640 units: NumericSuffix::Mm,
12641 },
12642 y: Number {
12643 value: 3.0,
12644 units: NumericSuffix::Mm,
12645 },
12646 },
12647 },
12648 FixedPoint {
12649 point: point1_id,
12650 position: Point2d {
12651 x: Number {
12652 value: 4.0,
12653 units: NumericSuffix::Mm,
12654 },
12655 y: Number {
12656 value: 5.0,
12657 units: NumericSuffix::Mm,
12658 },
12659 },
12660 },
12661 ],
12662 }),
12663 )
12664 .await
12665 .unwrap();
12666 insta::assert_snapshot!("test_add_fixed_multiple_points", src_delta.text.as_str());
12667 assert_eq!(
12668 scene_delta.new_graph.objects.len(),
12669 6,
12670 "{:#?}",
12671 scene_delta.new_graph.objects
12672 );
12673
12674 ctx.close().await;
12675 mock_ctx.close().await;
12676 }
12677
12678 #[tokio::test(flavor = "multi_thread")]
12679 async fn test_add_fixed_owned_point() {
12680 let initial_source = "\
12681sketch(on = XY) {
12682 line(start = [var 1, var 2], end = [var 3, var 4])
12683}
12684";
12685
12686 let program = Program::parse(initial_source).unwrap().0.unwrap();
12687
12688 let mut frontend = FrontendState::new();
12689
12690 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
12691 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
12692 let version = Version(0);
12693
12694 frontend.hack_set_program(&ctx, program).await.unwrap();
12695 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12696 let sketch_id = sketch_object.id;
12697 let sketch = expect_sketch(sketch_object);
12698 let line_start_id = *sketch.segments.first().unwrap();
12699
12700 let (src_delta, scene_delta) = frontend
12701 .add_constraint(
12702 &mock_ctx,
12703 version,
12704 sketch_id,
12705 Constraint::Fixed(Fixed {
12706 points: vec![FixedPoint {
12707 point: line_start_id,
12708 position: Point2d {
12709 x: Number {
12710 value: 2.0,
12711 units: NumericSuffix::Mm,
12712 },
12713 y: Number {
12714 value: 3.0,
12715 units: NumericSuffix::Mm,
12716 },
12717 },
12718 }],
12719 }),
12720 )
12721 .await
12722 .unwrap();
12723 insta::assert_snapshot!("test_add_fixed_owned_point", src_delta.text.as_str());
12724 assert_eq!(
12725 scene_delta.new_graph.objects.len(),
12726 6,
12727 "{:#?}",
12728 scene_delta.new_graph.objects
12729 );
12730
12731 ctx.close().await;
12732 mock_ctx.close().await;
12733 }
12734
12735 #[tokio::test(flavor = "multi_thread")]
12736 async fn test_radius_error_cases() {
12737 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
12738 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
12739 let version = Version(0);
12740
12741 let initial_source_point = "\
12743sketch(on = XY) {
12744 point(at = [var 1, var 2])
12745}
12746";
12747 let program_point = Program::parse(initial_source_point).unwrap().0.unwrap();
12748 let mut frontend_point = FrontendState::new();
12749 frontend_point.hack_set_program(&ctx, program_point).await.unwrap();
12750 let sketch_object_point = find_first_sketch_object(&frontend_point.scene_graph).unwrap();
12751 let sketch_id_point = sketch_object_point.id;
12752 let sketch_point = expect_sketch(sketch_object_point);
12753 let point_id = *sketch_point.segments.first().unwrap();
12754
12755 let constraint_point = Constraint::Radius(Radius {
12756 arc: point_id,
12757 radius: Number {
12758 value: 5.0,
12759 units: NumericSuffix::Mm,
12760 },
12761 label_position: None,
12762 source: Default::default(),
12763 });
12764 let result_point = frontend_point
12765 .add_constraint(&mock_ctx, version, sketch_id_point, constraint_point)
12766 .await;
12767 assert!(result_point.is_err(), "Single point should error for radius");
12768
12769 let initial_source_line = "\
12771sketch(on = XY) {
12772 line(start = [var 1, var 2], end = [var 3, var 4])
12773}
12774";
12775 let program_line = Program::parse(initial_source_line).unwrap().0.unwrap();
12776 let mut frontend_line = FrontendState::new();
12777 frontend_line.hack_set_program(&ctx, program_line).await.unwrap();
12778 let sketch_object_line = find_first_sketch_object(&frontend_line.scene_graph).unwrap();
12779 let sketch_id_line = sketch_object_line.id;
12780 let sketch_line = expect_sketch(sketch_object_line);
12781 let line_id = *sketch_line.segments.first().unwrap();
12782
12783 let constraint_line = Constraint::Radius(Radius {
12784 arc: line_id,
12785 radius: Number {
12786 value: 5.0,
12787 units: NumericSuffix::Mm,
12788 },
12789 label_position: None,
12790 source: Default::default(),
12791 });
12792 let result_line = frontend_line
12793 .add_constraint(&mock_ctx, version, sketch_id_line, constraint_line)
12794 .await;
12795 assert!(result_line.is_err(), "Single line segment should error for radius");
12796
12797 ctx.close().await;
12798 mock_ctx.close().await;
12799 }
12800
12801 #[tokio::test(flavor = "multi_thread")]
12802 async fn test_diameter_single_arc_segment() {
12803 let initial_source = "\
12804sketch(on = XY) {
12805 arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
12806}
12807";
12808
12809 let program = Program::parse(initial_source).unwrap().0.unwrap();
12810
12811 let mut frontend = FrontendState::new();
12812
12813 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
12814 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
12815 let version = Version(0);
12816
12817 frontend.hack_set_program(&ctx, program).await.unwrap();
12818 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12819 let sketch_id = sketch_object.id;
12820 let sketch = expect_sketch(sketch_object);
12821 let arc_id = sketch
12823 .segments
12824 .iter()
12825 .find(|&seg_id| {
12826 let obj = frontend.scene_graph.objects.get(seg_id.0);
12827 matches!(
12828 obj.map(|o| &o.kind),
12829 Some(ObjectKind::Segment {
12830 segment: Segment::Arc(_)
12831 })
12832 )
12833 })
12834 .unwrap();
12835
12836 let constraint = Constraint::Diameter(Diameter {
12837 arc: *arc_id,
12838 diameter: Number {
12839 value: 10.0,
12840 units: NumericSuffix::Mm,
12841 },
12842 label_position: None,
12843 source: Default::default(),
12844 });
12845 let (src_delta, scene_delta) = frontend
12846 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12847 .await
12848 .unwrap();
12849 insta::assert_snapshot!("test_diameter_single_arc_segment", src_delta.text.as_str());
12850 assert_eq!(
12851 scene_delta.new_graph.objects.len(),
12852 7, "{:#?}",
12854 scene_delta.new_graph.objects
12855 );
12856
12857 ctx.close().await;
12858 mock_ctx.close().await;
12859 }
12860
12861 #[tokio::test(flavor = "multi_thread")]
12862 async fn test_diameter_single_arc_segment_with_label_position() {
12863 let initial_source = "\
12864sketch(on = XY) {
12865 arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
12866}
12867";
12868
12869 let program = Program::parse(initial_source).unwrap().0.unwrap();
12870 let mut frontend = FrontendState::new();
12871 let mock_ctx = ExecutorContext::new_mock(None).await;
12872 let version = Version(0);
12873
12874 frontend.program = program.clone();
12875 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12876 frontend.update_state_after_exec(outcome, true);
12877 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12878 let sketch_id = sketch_object.id;
12879 let sketch = expect_sketch(sketch_object);
12880 let arc_id = sketch
12881 .segments
12882 .iter()
12883 .find(|&seg_id| {
12884 let obj = frontend.scene_graph.objects.get(seg_id.0);
12885 matches!(
12886 obj.map(|o| &o.kind),
12887 Some(ObjectKind::Segment {
12888 segment: Segment::Arc(_)
12889 })
12890 )
12891 })
12892 .unwrap();
12893
12894 let label_position = Point2d {
12895 x: Number {
12896 value: 10.0,
12897 units: NumericSuffix::Mm,
12898 },
12899 y: Number {
12900 value: 11.0,
12901 units: NumericSuffix::Mm,
12902 },
12903 };
12904 let constraint = Constraint::Diameter(Diameter {
12905 arc: *arc_id,
12906 diameter: Number {
12907 value: 10.0,
12908 units: NumericSuffix::Mm,
12909 },
12910 label_position: Some(label_position.clone()),
12911 source: Default::default(),
12912 });
12913 let (src_delta, scene_delta) = frontend
12914 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12915 .await
12916 .unwrap();
12917 insta::assert_snapshot!(
12918 "test_diameter_single_arc_segment_with_label_position",
12919 src_delta.text.as_str()
12920 );
12921
12922 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
12923 let sketch = expect_sketch(sketch_object);
12924 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
12925 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12926 panic!("Expected constraint object");
12927 };
12928 let Constraint::Diameter(diameter) = constraint else {
12929 panic!("Expected diameter constraint");
12930 };
12931 assert_eq!(diameter.label_position, Some(label_position));
12932
12933 mock_ctx.close().await;
12934 }
12935
12936 #[tokio::test(flavor = "multi_thread")]
12937 async fn test_edit_diameter_constraint_label_position() {
12938 let initial_source = "\
12939sketch(on = XY) {
12940 arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
12941 diameter(arc1) == 10mm
12942}
12943";
12944
12945 let program = Program::parse(initial_source).unwrap().0.unwrap();
12946 let mut frontend = FrontendState::new();
12947 let mock_ctx = ExecutorContext::new_mock(None).await;
12948 let version = Version(0);
12949
12950 frontend.program = program.clone();
12951 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12952 frontend.update_state_after_exec(outcome, true);
12953 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12954 let sketch_id = sketch_object.id;
12955 let sketch = expect_sketch(sketch_object);
12956 let constraint_id = sketch.constraints[0];
12957 let label_position = Point2d {
12958 x: Number {
12959 value: 10.0,
12960 units: NumericSuffix::Mm,
12961 },
12962 y: Number {
12963 value: 11.0,
12964 units: NumericSuffix::Mm,
12965 },
12966 };
12967
12968 let (src_delta, scene_delta) = frontend
12969 .edit_distance_constraint_label_position(
12970 &mock_ctx,
12971 version,
12972 sketch_id,
12973 constraint_id,
12974 label_position.clone(),
12975 vec![],
12976 )
12977 .await
12978 .unwrap();
12979 insta::assert_snapshot!("test_edit_diameter_constraint_label_position", src_delta.text.as_str());
12980
12981 let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
12982 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12983 panic!("Expected constraint object");
12984 };
12985 let Constraint::Diameter(diameter) = constraint else {
12986 panic!("Expected diameter constraint");
12987 };
12988 assert_eq!(diameter.label_position, Some(label_position));
12989
12990 mock_ctx.close().await;
12991 }
12992
12993 #[tokio::test(flavor = "multi_thread")]
12994 async fn test_diameter_error_cases() {
12995 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
12996 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
12997 let version = Version(0);
12998
12999 let initial_source_point = "\
13001sketch(on = XY) {
13002 point(at = [var 1, var 2])
13003}
13004";
13005 let program_point = Program::parse(initial_source_point).unwrap().0.unwrap();
13006 let mut frontend_point = FrontendState::new();
13007 frontend_point.hack_set_program(&ctx, program_point).await.unwrap();
13008 let sketch_object_point = find_first_sketch_object(&frontend_point.scene_graph).unwrap();
13009 let sketch_id_point = sketch_object_point.id;
13010 let sketch_point = expect_sketch(sketch_object_point);
13011 let point_id = *sketch_point.segments.first().unwrap();
13012
13013 let constraint_point = Constraint::Diameter(Diameter {
13014 arc: point_id,
13015 diameter: Number {
13016 value: 10.0,
13017 units: NumericSuffix::Mm,
13018 },
13019 label_position: None,
13020 source: Default::default(),
13021 });
13022 let result_point = frontend_point
13023 .add_constraint(&mock_ctx, version, sketch_id_point, constraint_point)
13024 .await;
13025 assert!(result_point.is_err(), "Single point should error for diameter");
13026
13027 let initial_source_line = "\
13029sketch(on = XY) {
13030 line(start = [var 1, var 2], end = [var 3, var 4])
13031}
13032";
13033 let program_line = Program::parse(initial_source_line).unwrap().0.unwrap();
13034 let mut frontend_line = FrontendState::new();
13035 frontend_line.hack_set_program(&ctx, program_line).await.unwrap();
13036 let sketch_object_line = find_first_sketch_object(&frontend_line.scene_graph).unwrap();
13037 let sketch_id_line = sketch_object_line.id;
13038 let sketch_line = expect_sketch(sketch_object_line);
13039 let line_id = *sketch_line.segments.first().unwrap();
13040
13041 let constraint_line = Constraint::Diameter(Diameter {
13042 arc: line_id,
13043 diameter: Number {
13044 value: 10.0,
13045 units: NumericSuffix::Mm,
13046 },
13047 label_position: None,
13048 source: Default::default(),
13049 });
13050 let result_line = frontend_line
13051 .add_constraint(&mock_ctx, version, sketch_id_line, constraint_line)
13052 .await;
13053 assert!(result_line.is_err(), "Single line segment should error for diameter");
13054
13055 ctx.close().await;
13056 mock_ctx.close().await;
13057 }
13058
13059 #[tokio::test(flavor = "multi_thread")]
13060 async fn test_line_horizontal() {
13061 let initial_source = "\
13062sketch(on = XY) {
13063 line(start = [var 1, var 2], end = [var 3, var 4])
13064}
13065";
13066
13067 let program = Program::parse(initial_source).unwrap().0.unwrap();
13068
13069 let mut frontend = FrontendState::new();
13070
13071 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13072 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
13073 let version = Version(0);
13074
13075 frontend.hack_set_program(&ctx, program).await.unwrap();
13076 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13077 let sketch_id = sketch_object.id;
13078 let sketch = expect_sketch(sketch_object);
13079 let line1_id = *sketch.segments.get(2).unwrap();
13080
13081 let constraint = Constraint::Horizontal(Horizontal::Line { line: line1_id });
13082 let (src_delta, scene_delta) = frontend
13083 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13084 .await
13085 .unwrap();
13086 insta::assert_snapshot!("test_line_horizontal", src_delta.text.as_str());
13087 assert_eq!(
13088 scene_delta.new_graph.objects.len(),
13089 6,
13090 "{:#?}",
13091 scene_delta.new_graph.objects
13092 );
13093
13094 ctx.close().await;
13095 mock_ctx.close().await;
13096 }
13097
13098 #[tokio::test(flavor = "multi_thread")]
13099 async fn test_control_point_spline_edge_horizontal() {
13100 let initial_source = "\
13101@settings(experimentalFeatures = allow)
13102splineSketch = sketch(on = XY) {
13103 controlPointSpline1 = controlPointSpline(points = [
13104 [var 0mm, var 0mm],
13105 [var 10mm, var 20mm],
13106 [var 20mm, var 0mm],
13107 ])
13108}
13109";
13110
13111 let program = Program::parse(initial_source).unwrap().0.unwrap();
13112
13113 let mut frontend = FrontendState::new();
13114
13115 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13116 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
13117 let version = Version(0);
13118
13119 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
13120 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13121 let sketch_id = sketch_object.id;
13122 let sketch = expect_sketch(sketch_object);
13123 let spline_id = sketch
13124 .segments
13125 .iter()
13126 .copied()
13127 .find(|seg_id| {
13128 matches!(
13129 &frontend.scene_graph.objects[seg_id.0].kind,
13130 ObjectKind::Segment {
13131 segment: Segment::ControlPointSpline(_)
13132 }
13133 )
13134 })
13135 .expect("Expected a control point spline segment in sketch");
13136 let edge_id = frontend
13137 .scene_graph
13138 .objects
13139 .iter()
13140 .find_map(|obj| match &obj.kind {
13141 ObjectKind::Segment {
13142 segment: Segment::Line(line),
13143 } if line.owner == Some(spline_id) => Some(obj.id),
13144 _ => None,
13145 })
13146 .expect("Expected an owned control-polygon edge");
13147
13148 let constraint = Constraint::Horizontal(Horizontal::Line { line: edge_id });
13149 let (src_delta, _) = frontend
13150 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13151 .await
13152 .unwrap();
13153 assert!(
13154 src_delta.text.contains("horizontal(controlPointSpline1.edges[0])"),
13155 "Expected horizontal constraint on spline edge, got: {}",
13156 src_delta.text
13157 );
13158
13159 ctx.close().await;
13160 mock_ctx.close().await;
13161 }
13162
13163 #[tokio::test(flavor = "multi_thread")]
13164 async fn test_control_point_spline_edge_angle() {
13165 let initial_source = "\
13166@settings(experimentalFeatures = allow)
13167splineSketch = sketch(on = XY) {
13168 controlPointSpline1 = controlPointSpline(points = [
13169 [var 0mm, var 0mm],
13170 [var 10mm, var 20mm],
13171 [var 20mm, var 0mm],
13172 ])
13173
13174 line1 = line(start = [var 40mm, var 0mm], end = [var 60mm, var 10mm])
13175}
13176";
13177
13178 let program = Program::parse(initial_source).unwrap().0.unwrap();
13179
13180 let mut frontend = FrontendState::new();
13181
13182 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13183 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
13184 let version = Version(0);
13185
13186 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
13187 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13188 let sketch_id = sketch_object.id;
13189 let sketch = expect_sketch(sketch_object);
13190 let spline_id = sketch
13191 .segments
13192 .iter()
13193 .copied()
13194 .find(|seg_id| {
13195 matches!(
13196 &frontend.scene_graph.objects[seg_id.0].kind,
13197 ObjectKind::Segment {
13198 segment: Segment::ControlPointSpline(_)
13199 }
13200 )
13201 })
13202 .expect("Expected a control point spline segment in sketch");
13203 let edge_id = frontend
13204 .scene_graph
13205 .objects
13206 .iter()
13207 .find_map(|obj| match &obj.kind {
13208 ObjectKind::Segment {
13209 segment: Segment::Line(line),
13210 } if line.owner == Some(spline_id) => Some(obj.id),
13211 _ => None,
13212 })
13213 .expect("Expected an owned control-polygon edge");
13214 let line1_id = frontend
13215 .scene_graph
13216 .objects
13217 .iter()
13218 .find_map(|obj| match &obj.kind {
13219 ObjectKind::Segment {
13220 segment: Segment::Line(line),
13221 } if line.owner.is_none() && obj.label == "line1" => Some(obj.id),
13222 _ => None,
13223 })
13224 .or_else(|| {
13225 sketch.segments.iter().copied().find(|seg_id| {
13226 matches!(
13227 &frontend.scene_graph.objects[seg_id.0].kind,
13228 ObjectKind::Segment {
13229 segment: Segment::Line(line),
13230 } if line.owner.is_none()
13231 )
13232 })
13233 })
13234 .expect("Expected a standalone line segment in sketch");
13235
13236 let constraint = Constraint::Angle(Angle {
13237 lines: vec![line1_id, edge_id],
13238 angle: Number {
13239 value: 30.0,
13240 units: NumericSuffix::Deg,
13241 },
13242 sector: None,
13243 inverse: None,
13244 label_position: None,
13245 source: Default::default(),
13246 });
13247 let (src_delta, _) = frontend
13248 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13249 .await
13250 .unwrap();
13251 assert!(
13252 src_delta
13253 .text
13254 .contains("angle([line1, controlPointSpline1.edges[0]]) == 30deg"),
13255 "Expected angle constraint on spline edge, got: {}",
13256 src_delta.text
13257 );
13258
13259 ctx.close().await;
13260 mock_ctx.close().await;
13261 }
13262
13263 #[tokio::test(flavor = "multi_thread")]
13264 async fn test_ui_scene_graph_hides_same_spline_coincident_constraints() {
13265 let initial_source = "\
13266@settings(experimentalFeatures = allow)
13267splineSketch = sketch(on = XY) {
13268 spline1 = controlPointSpline(points = [
13269 [var 0mm, var 0mm],
13270 [var 10mm, var 20mm],
13271 [var 20mm, var 0mm],
13272 ])
13273 line1 = line(start = [var 0mm, var 0mm], end = [var -10mm, var 0mm])
13274 coincident([spline1.controls[1], spline1.edges[0]])
13275 coincident([spline1.controls[0], line1])
13276}
13277";
13278
13279 let program = Program::parse(initial_source).unwrap().0.unwrap();
13280
13281 let mut frontend = FrontendState::new();
13282
13283 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13284 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
13285
13286 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
13287
13288 let ui_scene_graph = frontend.scene_graph_for_ui();
13289 let sketch_object = find_first_sketch_object(&ui_scene_graph).unwrap();
13290 let sketch = expect_sketch(sketch_object);
13291
13292 assert_eq!(
13293 sketch.constraints.len(),
13294 1,
13295 "Expected only the external coincident constraint to remain visible in the UI scene graph"
13296 );
13297
13298 let visible_constraints = ui_scene_graph
13299 .objects
13300 .iter()
13301 .filter_map(|object| match &object.kind {
13302 ObjectKind::Constraint {
13303 constraint: Constraint::Coincident(coincident),
13304 } => Some(coincident.clone()),
13305 _ => None,
13306 })
13307 .collect::<Vec<_>>();
13308
13309 assert_eq!(
13310 visible_constraints.len(),
13311 1,
13312 "Expected only one coincident constraint object in the UI scene graph"
13313 );
13314 assert_eq!(
13315 visible_constraints[0].get_segments().len(),
13316 2,
13317 "Expected the remaining visible coincident constraint to reference two segments"
13318 );
13319
13320 ctx.close().await;
13321 mock_ctx.close().await;
13322 }
13323
13324 #[tokio::test(flavor = "multi_thread")]
13325 async fn test_edit_control_point_spline_can_append_control_point() {
13326 let initial_source = "\
13327@settings(experimentalFeatures = allow)
13328splineSketch = sketch(on = XY) {
13329 controlPointSpline(points = [
13330 [var 0mm, var 0mm],
13331 [var 10mm, var 20mm],
13332 [var 20mm, var 0mm],
13333 ])
13334}
13335";
13336
13337 let program = Program::parse(initial_source).unwrap().0.unwrap();
13338
13339 let mut frontend = FrontendState::new();
13340
13341 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13342 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
13343 let version = Version(0);
13344
13345 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
13346 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13347 let sketch_id = sketch_object.id;
13348 let sketch = expect_sketch(sketch_object);
13349 let spline_id = sketch
13350 .segments
13351 .iter()
13352 .copied()
13353 .find(|seg_id| {
13354 matches!(
13355 &frontend.scene_graph.objects[seg_id.0].kind,
13356 ObjectKind::Segment {
13357 segment: Segment::ControlPointSpline(_)
13358 }
13359 )
13360 })
13361 .expect("Expected a control point spline segment in sketch");
13362
13363 let ctor = ControlPointSplineCtor {
13364 points: vec![
13365 Point2d {
13366 x: Expr::Var(Number {
13367 value: 0.0,
13368 units: NumericSuffix::Mm,
13369 }),
13370 y: Expr::Var(Number {
13371 value: 0.0,
13372 units: NumericSuffix::Mm,
13373 }),
13374 },
13375 Point2d {
13376 x: Expr::Var(Number {
13377 value: 10.0,
13378 units: NumericSuffix::Mm,
13379 }),
13380 y: Expr::Var(Number {
13381 value: 20.0,
13382 units: NumericSuffix::Mm,
13383 }),
13384 },
13385 Point2d {
13386 x: Expr::Var(Number {
13387 value: 20.0,
13388 units: NumericSuffix::Mm,
13389 }),
13390 y: Expr::Var(Number {
13391 value: 0.0,
13392 units: NumericSuffix::Mm,
13393 }),
13394 },
13395 Point2d {
13396 x: Expr::Var(Number {
13397 value: 30.0,
13398 units: NumericSuffix::Mm,
13399 }),
13400 y: Expr::Var(Number {
13401 value: 10.0,
13402 units: NumericSuffix::Mm,
13403 }),
13404 },
13405 ],
13406 construction: None,
13407 };
13408
13409 let segments = vec![ExistingSegmentCtor {
13410 id: spline_id,
13411 ctor: SegmentCtor::ControlPointSpline(ctor),
13412 }];
13413 let (src_delta, scene_delta) = frontend
13414 .edit_segments(&mock_ctx, version, sketch_id, segments)
13415 .await
13416 .unwrap();
13417
13418 assert!(
13419 src_delta.text.contains("[var 30mm, var 10mm]"),
13420 "Expected appended spline control point in source, got: {}",
13421 src_delta.text
13422 );
13423
13424 assert!(
13425 scene_delta.invalidates_ids,
13426 "Expected appending a spline control point to invalidate ids"
13427 );
13428 let updated_spline = scene_delta
13429 .new_graph
13430 .objects
13431 .iter()
13432 .find_map(|obj| match &obj.kind {
13433 ObjectKind::Segment {
13434 segment: Segment::ControlPointSpline(updated_spline),
13435 } if updated_spline.controls.len() == 4 => Some(updated_spline),
13436 _ => None,
13437 })
13438 .expect("Expected edited scene graph to contain a four-point control point spline");
13439 assert_eq!(
13440 updated_spline.controls.len(),
13441 4,
13442 "Expected edited spline to expose four control points"
13443 );
13444
13445 ctx.close().await;
13446 mock_ctx.close().await;
13447 }
13448
13449 #[tokio::test(flavor = "multi_thread")]
13450 async fn test_line_vertical() {
13451 let initial_source = "\
13452sketch(on = XY) {
13453 line(start = [var 1, var 2], end = [var 3, var 4])
13454}
13455";
13456
13457 let program = Program::parse(initial_source).unwrap().0.unwrap();
13458
13459 let mut frontend = FrontendState::new();
13460
13461 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13462 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
13463 let version = Version(0);
13464
13465 frontend.hack_set_program(&ctx, program).await.unwrap();
13466 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13467 let sketch_id = sketch_object.id;
13468 let sketch = expect_sketch(sketch_object);
13469 let line1_id = *sketch.segments.get(2).unwrap();
13470
13471 let constraint = Constraint::Vertical(Vertical::Line { line: line1_id });
13472 let (src_delta, scene_delta) = frontend
13473 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13474 .await
13475 .unwrap();
13476 insta::assert_snapshot!("test_line_vertical", src_delta.text.as_str());
13477 assert_eq!(
13478 scene_delta.new_graph.objects.len(),
13479 6,
13480 "{:#?}",
13481 scene_delta.new_graph.objects
13482 );
13483
13484 ctx.close().await;
13485 mock_ctx.close().await;
13486 }
13487
13488 #[tokio::test(flavor = "multi_thread")]
13489 async fn test_points_vertical() {
13490 let initial_source = "\
13491sketch001 = sketch(on = XY) {
13492 p0 = point(at = [var -2.23mm, var 3.1mm])
13493 pf = point(at = [4, 4])
13494}
13495";
13496
13497 let program = Program::parse(initial_source).unwrap().0.unwrap();
13498
13499 let mut frontend = FrontendState::new();
13500
13501 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13502 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
13503 let version = Version(0);
13504
13505 frontend.hack_set_program(&ctx, program).await.unwrap();
13506 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13507 let sketch_id = sketch_object.id;
13508 let sketch = expect_sketch(sketch_object);
13509 let point_ids = vec![
13510 sketch.segments.first().unwrap().to_owned(),
13511 sketch.segments.get(1).unwrap().to_owned(),
13512 ];
13513
13514 let constraint = Constraint::Vertical(Vertical::Points {
13515 points: point_ids.into_iter().map(ConstraintSegment::from).collect(),
13516 });
13517 let (src_delta, scene_delta) = frontend
13518 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13519 .await
13520 .unwrap();
13521 insta::assert_snapshot!("test_points_vertical", src_delta.text.as_str());
13522 assert_eq!(
13523 scene_delta.new_graph.objects.len(),
13524 5,
13525 "{:#?}",
13526 scene_delta.new_graph.objects
13527 );
13528
13529 ctx.close().await;
13530 mock_ctx.close().await;
13531 }
13532
13533 #[tokio::test(flavor = "multi_thread")]
13534 async fn test_points_horizontal() {
13535 let initial_source = "\
13536sketch001 = sketch(on = XY) {
13537 p0 = point(at = [var -2.23mm, var 3.1mm])
13538 pf = point(at = [4, 4])
13539}
13540";
13541
13542 let program = Program::parse(initial_source).unwrap().0.unwrap();
13543
13544 let mut frontend = FrontendState::new();
13545
13546 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13547 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
13548 let version = Version(0);
13549
13550 frontend.hack_set_program(&ctx, program).await.unwrap();
13551 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13552 let sketch_id = sketch_object.id;
13553 let sketch = expect_sketch(sketch_object);
13554 let point_ids = vec![
13555 sketch.segments.first().unwrap().to_owned(),
13556 sketch.segments.get(1).unwrap().to_owned(),
13557 ];
13558
13559 let constraint = Constraint::Horizontal(Horizontal::Points {
13560 points: point_ids.into_iter().map(ConstraintSegment::from).collect(),
13561 });
13562 let (src_delta, scene_delta) = frontend
13563 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13564 .await
13565 .unwrap();
13566 insta::assert_snapshot!("test_points_horizontal", src_delta.text.as_str());
13567 assert_eq!(
13568 scene_delta.new_graph.objects.len(),
13569 5,
13570 "{:#?}",
13571 scene_delta.new_graph.objects
13572 );
13573
13574 ctx.close().await;
13575 mock_ctx.close().await;
13576 }
13577
13578 #[tokio::test(flavor = "multi_thread")]
13579 async fn test_point_horizontal_with_origin() {
13580 let initial_source = "\
13581sketch001 = sketch(on = XY) {
13582 p0 = point(at = [var -2.23mm, var 3.1mm])
13583}
13584";
13585
13586 let program = Program::parse(initial_source).unwrap().0.unwrap();
13587
13588 let mut frontend = FrontendState::new();
13589
13590 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13591 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
13592 let version = Version(0);
13593
13594 frontend.hack_set_program(&ctx, program).await.unwrap();
13595 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13596 let sketch_id = sketch_object.id;
13597 let sketch = expect_sketch(sketch_object);
13598 let point_id = *sketch.segments.first().unwrap();
13599
13600 let constraint = Constraint::Horizontal(Horizontal::Points {
13601 points: vec![ConstraintSegment::from(point_id), ConstraintSegment::ORIGIN],
13602 });
13603 let (src_delta, scene_delta) = frontend
13604 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13605 .await
13606 .unwrap();
13607 insta::assert_snapshot!("test_point_horizontal_with_origin", src_delta.text.as_str());
13608 assert_eq!(
13609 scene_delta.new_graph.objects.len(),
13610 4,
13611 "{:#?}",
13612 scene_delta.new_graph.objects
13613 );
13614
13615 ctx.close().await;
13616 mock_ctx.close().await;
13617 }
13618
13619 #[tokio::test(flavor = "multi_thread")]
13620 async fn test_lines_equal_length() {
13621 let initial_source = "\
13622sketch(on = XY) {
13623 line(start = [var 1, var 2], end = [var 3, var 4])
13624 line(start = [var 5, var 6], end = [var 7, var 8])
13625}
13626";
13627
13628 let program = Program::parse(initial_source).unwrap().0.unwrap();
13629
13630 let mut frontend = FrontendState::new();
13631
13632 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13633 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
13634 let version = Version(0);
13635
13636 frontend.hack_set_program(&ctx, program).await.unwrap();
13637 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13638 let sketch_id = sketch_object.id;
13639 let sketch = expect_sketch(sketch_object);
13640 let line1_id = *sketch.segments.get(2).unwrap();
13641 let line2_id = *sketch.segments.get(5).unwrap();
13642
13643 let constraint = Constraint::LinesEqualLength(LinesEqualLength {
13644 lines: vec![line1_id, line2_id],
13645 });
13646 let (src_delta, scene_delta) = frontend
13647 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13648 .await
13649 .unwrap();
13650 insta::assert_snapshot!("test_lines_equal_length", src_delta.text.as_str());
13651 assert_eq!(
13652 scene_delta.new_graph.objects.len(),
13653 9,
13654 "{:#?}",
13655 scene_delta.new_graph.objects
13656 );
13657
13658 ctx.close().await;
13659 mock_ctx.close().await;
13660 }
13661
13662 #[tokio::test(flavor = "multi_thread")]
13663 async fn test_add_constraint_multi_line_equal_length() {
13664 let initial_source = "\
13665sketch(on = XY) {
13666 line(start = [var 1, var 2], end = [var 3, var 4])
13667 line(start = [var 5, var 6], end = [var 7, var 8])
13668 line(start = [var 9, var 10], end = [var 11, var 12])
13669}
13670";
13671
13672 let program = Program::parse(initial_source).unwrap().0.unwrap();
13673
13674 let mut frontend = FrontendState::new();
13675 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13676 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
13677 let version = Version(0);
13678
13679 frontend.hack_set_program(&ctx, program).await.unwrap();
13680 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13681 let sketch_id = sketch_object.id;
13682 let sketch = expect_sketch(sketch_object);
13683 let line1_id = *sketch.segments.get(2).unwrap();
13684 let line2_id = *sketch.segments.get(5).unwrap();
13685 let line3_id = *sketch.segments.get(8).unwrap();
13686
13687 let constraint = Constraint::LinesEqualLength(LinesEqualLength {
13688 lines: vec![line1_id, line2_id, line3_id],
13689 });
13690 let (src_delta, scene_delta) = frontend
13691 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13692 .await
13693 .unwrap();
13694 insta::assert_snapshot!("test_add_constraint_multi_line_equal_length", src_delta.text.as_str());
13695 let constraints = scene_delta
13696 .new_graph
13697 .objects
13698 .iter()
13699 .filter_map(|obj| {
13700 let ObjectKind::Constraint { constraint } = &obj.kind else {
13701 return None;
13702 };
13703 Some(constraint)
13704 })
13705 .collect::<Vec<_>>();
13706
13707 assert_eq!(constraints.len(), 1, "{:#?}", frontend.scene_graph.objects);
13708 let Constraint::LinesEqualLength(lines_equal_length) = constraints[0] else {
13709 panic!("expected equal length constraint, got {:?}", constraints[0]);
13710 };
13711 assert_eq!(lines_equal_length.lines.len(), 3);
13712
13713 ctx.close().await;
13714 mock_ctx.close().await;
13715 }
13716
13717 #[tokio::test(flavor = "multi_thread")]
13718 async fn test_lines_parallel() {
13719 let initial_source = "\
13720sketch(on = XY) {
13721 line(start = [var 1, var 2], end = [var 3, var 4])
13722 line(start = [var 5, var 6], end = [var 7, var 8])
13723}
13724";
13725
13726 let program = Program::parse(initial_source).unwrap().0.unwrap();
13727
13728 let mut frontend = FrontendState::new();
13729
13730 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13731 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
13732 let version = Version(0);
13733
13734 frontend.hack_set_program(&ctx, program).await.unwrap();
13735 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13736 let sketch_id = sketch_object.id;
13737 let sketch = expect_sketch(sketch_object);
13738 let line1_id = *sketch.segments.get(2).unwrap();
13739 let line2_id = *sketch.segments.get(5).unwrap();
13740
13741 let constraint = Constraint::Parallel(Parallel {
13742 lines: vec![line1_id, line2_id],
13743 });
13744 let (src_delta, scene_delta) = frontend
13745 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13746 .await
13747 .unwrap();
13748 insta::assert_snapshot!("test_lines_parallel", src_delta.text.as_str());
13749 assert_eq!(
13750 scene_delta.new_graph.objects.len(),
13751 9,
13752 "{:#?}",
13753 scene_delta.new_graph.objects
13754 );
13755
13756 ctx.close().await;
13757 mock_ctx.close().await;
13758 }
13759
13760 #[tokio::test(flavor = "multi_thread")]
13761 async fn test_lines_parallel_multiline() {
13762 let initial_source = "\
13763sketch(on = XY) {
13764 line(start = [var 1, var 2], end = [var 3, var 4])
13765 line(start = [var 5, var 6], end = [var 7, var 8])
13766 line(start = [var 9, var 10], end = [var 11, var 12])
13767}
13768";
13769
13770 let program = Program::parse(initial_source).unwrap().0.unwrap();
13771
13772 let mut frontend = FrontendState::new();
13773
13774 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13775 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
13776 let version = Version(0);
13777
13778 frontend.hack_set_program(&ctx, program).await.unwrap();
13779 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13780 let sketch_id = sketch_object.id;
13781 let sketch = expect_sketch(sketch_object);
13782 let line1_id = *sketch.segments.get(2).unwrap();
13783 let line2_id = *sketch.segments.get(5).unwrap();
13784 let line3_id = *sketch.segments.get(8).unwrap();
13785
13786 let constraint = Constraint::Parallel(Parallel {
13787 lines: vec![line1_id, line2_id, line3_id],
13788 });
13789 let (src_delta, scene_delta) = frontend
13790 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13791 .await
13792 .unwrap();
13793 insta::assert_snapshot!("test_lines_parallel_multiline", src_delta.text.as_str());
13794
13795 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
13796 let sketch = expect_sketch(sketch_object);
13797 assert_eq!(sketch.constraints.len(), 1);
13798
13799 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
13800 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
13801 panic!("Expected constraint object");
13802 };
13803 let Constraint::Parallel(parallel) = constraint else {
13804 panic!("Expected parallel constraint");
13805 };
13806 assert_eq!(parallel.lines.len(), 3);
13807
13808 ctx.close().await;
13809 mock_ctx.close().await;
13810 }
13811
13812 #[tokio::test(flavor = "multi_thread")]
13813 async fn test_lines_perpendicular() {
13814 let initial_source = "\
13815sketch(on = XY) {
13816 line(start = [var 1, var 2], end = [var 3, var 4])
13817 line(start = [var 5, var 6], end = [var 7, var 8])
13818}
13819";
13820
13821 let program = Program::parse(initial_source).unwrap().0.unwrap();
13822
13823 let mut frontend = FrontendState::new();
13824
13825 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13826 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
13827 let version = Version(0);
13828
13829 frontend.hack_set_program(&ctx, program).await.unwrap();
13830 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13831 let sketch_id = sketch_object.id;
13832 let sketch = expect_sketch(sketch_object);
13833 let line1_id = *sketch.segments.get(2).unwrap();
13834 let line2_id = *sketch.segments.get(5).unwrap();
13835
13836 let constraint = Constraint::Perpendicular(Perpendicular {
13837 lines: vec![line1_id, line2_id],
13838 });
13839 let (src_delta, scene_delta) = frontend
13840 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13841 .await
13842 .unwrap();
13843 insta::assert_snapshot!("test_lines_perpendicular", src_delta.text.as_str());
13844 assert_eq!(
13845 scene_delta.new_graph.objects.len(),
13846 9,
13847 "{:#?}",
13848 scene_delta.new_graph.objects
13849 );
13850
13851 ctx.close().await;
13852 mock_ctx.close().await;
13853 }
13854
13855 #[tokio::test(flavor = "multi_thread")]
13856 async fn test_lines_angle() {
13857 let initial_source = "\
13858sketch(on = XY) {
13859 line(start = [var 1, var 2], end = [var 3, var 4])
13860 line(start = [var 5, var 6], end = [var 7, var 8])
13861}
13862";
13863
13864 let program = Program::parse(initial_source).unwrap().0.unwrap();
13865
13866 let mut frontend = FrontendState::new();
13867
13868 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13869 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
13870 let version = Version(0);
13871
13872 frontend.hack_set_program(&ctx, program).await.unwrap();
13873 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13874 let sketch_id = sketch_object.id;
13875 let sketch = expect_sketch(sketch_object);
13876 let line1_id = *sketch.segments.get(2).unwrap();
13877 let line2_id = *sketch.segments.get(5).unwrap();
13878
13879 let constraint = Constraint::Angle(Angle {
13880 lines: vec![line1_id, line2_id],
13881 angle: Number {
13882 value: 30.0,
13883 units: NumericSuffix::Deg,
13884 },
13885 sector: None,
13886 inverse: None,
13887 label_position: None,
13888 source: Default::default(),
13889 });
13890 let (src_delta, scene_delta) = frontend
13891 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13892 .await
13893 .unwrap();
13894 insta::assert_snapshot!("test_lines_angle", src_delta.text.as_str());
13895 assert_eq!(
13896 scene_delta.new_graph.objects.len(),
13897 9,
13898 "{:#?}",
13899 scene_delta.new_graph.objects
13900 );
13901
13902 ctx.close().await;
13903 mock_ctx.close().await;
13904 }
13905
13906 #[tokio::test(flavor = "multi_thread")]
13907 async fn test_lines_angle_with_sector_uses_angle_dimension() {
13908 let initial_source = "\
13909sketch(on = XY) {
13910 line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
13911 line(start = [var 0mm, var 0mm], end = [var 0mm, var 4mm])
13912}
13913";
13914
13915 let program = Program::parse(initial_source).unwrap().0.unwrap();
13916
13917 let mut frontend = FrontendState::new();
13918
13919 let mock_ctx = ExecutorContext::new_mock(None).await;
13920 let version = Version(0);
13921
13922 frontend.program = program.clone();
13923 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
13924 frontend.update_state_after_exec(outcome, true);
13925 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13926 let sketch_id = sketch_object.id;
13927 let sketch = expect_sketch(sketch_object);
13928 let line1_id = *sketch.segments.get(2).unwrap();
13929 let line2_id = *sketch.segments.get(5).unwrap();
13930
13931 let constraint = Constraint::Angle(Angle {
13932 lines: vec![line1_id, line2_id],
13933 angle: Number {
13934 value: 270.0,
13935 units: NumericSuffix::Deg,
13936 },
13937 sector: Some(1),
13938 inverse: Some(true),
13939 label_position: Some(Point2d {
13940 x: Number {
13941 value: -0.73,
13942 units: NumericSuffix::Mm,
13943 },
13944 y: Number {
13945 value: 0.75,
13946 units: NumericSuffix::Mm,
13947 },
13948 }),
13949 source: Default::default(),
13950 });
13951 let (src_delta, _) = frontend
13952 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13953 .await
13954 .unwrap();
13955 assert_eq!(
13956 src_delta.text.as_str(),
13957 "\
13958sketch(on = XY) {
13959 line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
13960 line2 = line(start = [var 0mm, var 0mm], end = [var 0mm, var 4mm])
13961 angleDimension(
13962 lines = [line1, line2],
13963 sector = 1,
13964 inverse = true,
13965 labelPosition = [-0.73mm, 0.75mm],
13966 ) == 270deg
13967}
13968"
13969 );
13970
13971 mock_ctx.close().await;
13972 }
13973
13974 #[tokio::test(flavor = "multi_thread")]
13975 async fn test_segments_tangent() {
13976 let initial_source = "\
13977sketch(on = XY) {
13978 line(start = [var 1, var 2], end = [var 3, var 4])
13979 arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
13980}
13981";
13982
13983 let program = Program::parse(initial_source).unwrap().0.unwrap();
13984
13985 let mut frontend = FrontendState::new();
13986
13987 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13988 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
13989 let version = Version(0);
13990
13991 frontend.hack_set_program(&ctx, program).await.unwrap();
13992 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13993 let sketch_id = sketch_object.id;
13994 let sketch = expect_sketch(sketch_object);
13995 let line1_id = *sketch.segments.get(2).unwrap();
13996 let arc1_id = *sketch.segments.get(6).unwrap();
13997
13998 let constraint = Constraint::Tangent(Tangent {
13999 input: vec![line1_id, arc1_id],
14000 });
14001 let (src_delta, scene_delta) = frontend
14002 .add_constraint(&mock_ctx, version, sketch_id, constraint)
14003 .await
14004 .unwrap();
14005 insta::assert_snapshot!("test_segments_tangent", src_delta.text.as_str());
14006 assert_eq!(
14007 scene_delta.new_graph.objects.len(),
14008 10,
14009 "{:#?}",
14010 scene_delta.new_graph.objects
14011 );
14012
14013 ctx.close().await;
14014 mock_ctx.close().await;
14015 }
14016
14017 #[tokio::test(flavor = "multi_thread")]
14018 async fn test_point_midpoint() {
14019 let initial_source = "\
14020sketch(on = XY) {
14021 point(at = [var 1, var 1])
14022 line(start = [var 0, var 0], end = [var 6, var 4])
14023}
14024";
14025
14026 let program = Program::parse(initial_source).unwrap().0.unwrap();
14027
14028 let mut frontend = FrontendState::new();
14029
14030 let ctx = ExecutorContext::new_mock(None).await;
14031 let version = Version(0);
14032
14033 frontend.program = program.clone();
14034 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14035 frontend.update_state_after_exec(outcome, true);
14036 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14037 let sketch_id = sketch_object.id;
14038 let sketch = expect_sketch(sketch_object);
14039 let point_id = *sketch.segments.first().unwrap();
14040 let line_id = *sketch.segments.get(3).unwrap();
14041
14042 let constraint = Constraint::Midpoint(Midpoint {
14043 point: ConstraintSegment::from(point_id),
14044 segment: line_id,
14045 });
14046 let (src_delta, scene_delta) = frontend
14047 .add_constraint(&ctx, version, sketch_id, constraint)
14048 .await
14049 .unwrap();
14050 insta::assert_snapshot!("test_point_midpoint", src_delta.text.as_str());
14051 assert_eq!(
14052 scene_delta.new_graph.objects.len(),
14053 7,
14054 "{:#?}",
14055 scene_delta.new_graph.objects
14056 );
14057
14058 ctx.close().await;
14059 }
14060
14061 #[tokio::test(flavor = "multi_thread")]
14062 async fn test_segments_symmetric() {
14063 let initial_source = "\
14064sketch(on = XY) {
14065 line(start = [var 0, var 0], end = [var 0, var 4])
14066 line(start = [var 4, var 0], end = [var 4, var 4])
14067 line(start = [var 2, var -1], end = [var 2, var 5])
14068}
14069";
14070
14071 let program = Program::parse(initial_source).unwrap().0.unwrap();
14072
14073 let mut frontend = FrontendState::new();
14074
14075 let ctx = ExecutorContext::new_mock(None).await;
14076 let version = Version(0);
14077
14078 frontend.program = program.clone();
14079 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14080 frontend.update_state_after_exec(outcome, true);
14081 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14082 let sketch_id = sketch_object.id;
14083 let sketch = expect_sketch(sketch_object);
14084 let line1_id = *sketch.segments.get(2).unwrap();
14085 let line2_id = *sketch.segments.get(5).unwrap();
14086 let axis_id = *sketch.segments.get(8).unwrap();
14087
14088 let constraint = Constraint::Symmetric(Symmetric {
14089 input: vec![line1_id, line2_id],
14090 axis: axis_id,
14091 });
14092 let (src_delta, scene_delta) = frontend
14093 .add_constraint(&ctx, version, sketch_id, constraint)
14094 .await
14095 .unwrap();
14096 insta::assert_snapshot!("test_segments_symmetric", src_delta.text.as_str());
14097 assert_eq!(
14098 scene_delta.new_graph.objects.len(),
14099 12,
14100 "{:#?}",
14101 scene_delta.new_graph.objects
14102 );
14103
14104 ctx.close().await;
14105 }
14106
14107 #[tokio::test(flavor = "multi_thread")]
14108 async fn test_point_arc_midpoint() {
14109 let initial_source = "\
14110sketch(on = XY) {
14111 point(at = [var 6, var 3])
14112 arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
14113}
14114";
14115
14116 let program = Program::parse(initial_source).unwrap().0.unwrap();
14117
14118 let mut frontend = FrontendState::new();
14119
14120 let ctx = ExecutorContext::new_mock(None).await;
14121 let version = Version(0);
14122
14123 frontend.program = program.clone();
14124 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14125 frontend.update_state_after_exec(outcome, true);
14126 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14127 let sketch_id = sketch_object.id;
14128 let sketch = expect_sketch(sketch_object);
14129 let point_id = *sketch.segments.first().unwrap();
14130 let arc_id = *sketch.segments.get(4).unwrap();
14131
14132 let constraint = Constraint::Midpoint(Midpoint {
14133 point: ConstraintSegment::from(point_id),
14134 segment: arc_id,
14135 });
14136 let (src_delta, scene_delta) = frontend
14137 .add_constraint(&ctx, version, sketch_id, constraint)
14138 .await
14139 .unwrap();
14140 insta::assert_snapshot!("test_point_arc_midpoint", src_delta.text.as_str());
14141 assert_eq!(
14142 scene_delta.new_graph.objects.len(),
14143 8,
14144 "{:#?}",
14145 scene_delta.new_graph.objects
14146 );
14147
14148 ctx.close().await;
14149 }
14150
14151 #[tokio::test(flavor = "multi_thread")]
14152 async fn test_origin_line_midpoint() {
14153 let initial_source = "\
14154sketch(on = XY) {
14155 line(start = [var 0, var 0], end = [var 6, var 4])
14156}
14157";
14158
14159 let program = Program::parse(initial_source).unwrap().0.unwrap();
14160
14161 let mut frontend = FrontendState::new();
14162
14163 let ctx = ExecutorContext::new_mock(None).await;
14164 let version = Version(0);
14165
14166 frontend.program = program.clone();
14167 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14168 frontend.update_state_after_exec(outcome, true);
14169 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14170 let sketch_id = sketch_object.id;
14171 let sketch = expect_sketch(sketch_object);
14172 let line_id = *sketch.segments.get(2).unwrap();
14173
14174 let constraint = Constraint::Midpoint(Midpoint {
14175 point: ConstraintSegment::ORIGIN,
14176 segment: line_id,
14177 });
14178 let (src_delta, scene_delta) = frontend
14179 .add_constraint(&ctx, version, sketch_id, constraint)
14180 .await
14181 .unwrap();
14182 insta::assert_snapshot!("test_origin_line_midpoint", src_delta.text.as_str());
14183 assert_eq!(
14184 scene_delta.new_graph.objects.len(),
14185 6,
14186 "{:#?}",
14187 scene_delta.new_graph.objects
14188 );
14189
14190 ctx.close().await;
14191 }
14192
14193 #[tokio::test(flavor = "multi_thread")]
14194 async fn test_origin_arc_midpoint() {
14195 let initial_source = "\
14196sketch(on = XY) {
14197 arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
14198}
14199";
14200
14201 let program = Program::parse(initial_source).unwrap().0.unwrap();
14202
14203 let mut frontend = FrontendState::new();
14204
14205 let ctx = ExecutorContext::new_mock(None).await;
14206 let version = Version(0);
14207
14208 frontend.program = program.clone();
14209 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14210 frontend.update_state_after_exec(outcome, true);
14211 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14212 let sketch_id = sketch_object.id;
14213 let sketch = expect_sketch(sketch_object);
14214 let arc_id = *sketch.segments.get(3).unwrap();
14215
14216 let constraint = Constraint::Midpoint(Midpoint {
14217 point: ConstraintSegment::ORIGIN,
14218 segment: arc_id,
14219 });
14220 let (src_delta, scene_delta) = frontend
14221 .add_constraint(&ctx, version, sketch_id, constraint)
14222 .await
14223 .unwrap();
14224 insta::assert_snapshot!("test_origin_arc_midpoint", src_delta.text.as_str());
14225 assert_eq!(
14226 scene_delta.new_graph.objects.len(),
14227 7,
14228 "{:#?}",
14229 scene_delta.new_graph.objects
14230 );
14231
14232 ctx.close().await;
14233 }
14234
14235 #[tokio::test(flavor = "multi_thread")]
14236 async fn test_segments_symmetric_arcs() {
14237 let initial_source = "\
14238sketch(on = XY) {
14239 arc(start = [var -15, var 0], end = [var -10, var 5], center = [var -10, var 0])
14240 arc(start = [var 6, var 2], end = [var 12, var -4], center = [var 8, var 1])
14241 line(start = [var 0, var -10], end = [var 0, var 10])
14242}
14243";
14244
14245 let program = Program::parse(initial_source).unwrap().0.unwrap();
14246
14247 let mut frontend = FrontendState::new();
14248
14249 let ctx = ExecutorContext::new_mock(None).await;
14250 let version = Version(0);
14251
14252 frontend.program = program.clone();
14253 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14254 frontend.update_state_after_exec(outcome, true);
14255 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14256 let sketch_id = sketch_object.id;
14257 let sketch = expect_sketch(sketch_object);
14258 let arc1_id = *sketch.segments.get(3).unwrap();
14259 let arc2_id = *sketch.segments.get(7).unwrap();
14260 let axis_id = *sketch.segments.get(10).unwrap();
14261
14262 let constraint = Constraint::Symmetric(Symmetric {
14263 input: vec![arc1_id, arc2_id],
14264 axis: axis_id,
14265 });
14266 let (src_delta, scene_delta) = frontend
14267 .add_constraint(&ctx, version, sketch_id, constraint)
14268 .await
14269 .unwrap();
14270 insta::assert_snapshot!("test_segments_symmetric_arcs", src_delta.text.as_str());
14271 assert_eq!(
14272 scene_delta.new_graph.objects.len(),
14273 14,
14274 "{:#?}",
14275 scene_delta.new_graph.objects
14276 );
14277
14278 ctx.close().await;
14279 }
14280
14281 #[tokio::test(flavor = "multi_thread")]
14282 async fn test_sketch_on_face_simple() {
14283 let initial_source = "\
14284len = 2mm
14285cube = startSketchOn(XY)
14286 |> startProfile(at = [0, 0])
14287 |> line(end = [len, 0], tag = $side)
14288 |> line(end = [0, len])
14289 |> line(end = [-len, 0])
14290 |> line(end = [0, -len])
14291 |> close()
14292 |> extrude(length = len)
14293
14294face = faceOf(cube, face = side)
14295";
14296
14297 let program = Program::parse(initial_source).unwrap().0.unwrap();
14298
14299 let mut frontend = FrontendState::new();
14300
14301 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
14302 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
14303 let version = Version(0);
14304
14305 frontend.hack_set_program(&ctx, program).await.unwrap();
14306 let face_object = find_first_face_object(&frontend.scene_graph).unwrap();
14307 let face_id = face_object.id;
14308
14309 let sketch_args = SketchCtor {
14310 on: Plane::Object(face_id),
14311 };
14312 let (_src_delta, scene_delta, sketch_id) = frontend
14313 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14314 .await
14315 .unwrap();
14316 assert_eq!(sketch_id, ObjectId(2));
14317 assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
14318 let sketch_object = &scene_delta.new_graph.objects[2];
14319 assert_eq!(sketch_object.id, ObjectId(2));
14320 assert_eq!(
14321 sketch_object.kind,
14322 ObjectKind::Sketch(Sketch {
14323 args: SketchCtor {
14324 on: Plane::Object(face_id),
14325 },
14326 plane: face_id,
14327 segments: vec![],
14328 constraints: vec![],
14329 })
14330 );
14331 assert_eq!(scene_delta.new_graph.objects.len(), 8);
14332
14333 ctx.close().await;
14334 mock_ctx.close().await;
14335 }
14336
14337 #[tokio::test(flavor = "multi_thread")]
14338 async fn test_new_sketch_on_primitive_index_face() {
14339 let initial_source = "\
14340@settings(kclVersion = 2.0)
14341
14342sketch001 = sketch(on = XY) {
14343 circle1 = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
14344}
14345extrude001 = extrude(region(point = [0mm, 0mm], sketch = sketch001), length = 5, tagEnd = $capEnd001)
14346shell001 = shell(extrude001, faces = capEnd001, thickness = 1)";
14347 let program = Program::parse(initial_source).unwrap().0.unwrap();
14348 let ctx = ExecutorContext::new_mock(None).await;
14349 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14350 let solid_id = match outcome.variables.get("shell001") {
14351 Some(KclValueView::Solid { value }) => value.id,
14352 value => panic!("expected shell001 to be a solid, got {value:?}"),
14353 };
14354 let solid_references = solid_references_from_variables(&program.ast, &outcome.variables);
14355
14356 let mut ast = program.ast;
14357 let scene_graph = SceneGraph::empty(ProjectId(0), FileId(0), Version(0));
14358 let face_expr = sketch_on_ast_expr(
14359 &mut ast,
14360 &scene_graph,
14361 &solid_references,
14362 &Plane::PrimitiveFace(kcl_api::PrimitiveFacePlane { solid_id, index: 6 }),
14363 )
14364 .unwrap();
14365 let face_decl = ast::VariableDeclaration::new(
14366 ast::VariableDeclarator::new("face001", face_expr),
14367 ast::ItemVisibility::Default,
14368 ast::VariableKind::Const,
14369 );
14370 ast.body
14371 .push(ast::BodyItem::VariableDeclaration(BoxNode::new(ast::Node::no_src(
14372 face_decl,
14373 ))));
14374 let face_source = source_from_ast(&ast);
14375 let new_source = format!("{face_source}sketch002 = sketch(on = face001) {{\n}}\n");
14376 insta::assert_snapshot!("test_new_sketch_on_primitive_index_face", new_source);
14377
14378 let program = Program::parse(&new_source).unwrap().0.unwrap();
14379 ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14380 ctx.close().await;
14381 }
14382
14383 #[tokio::test(flavor = "multi_thread")]
14384 async fn test_sketch_on_wall_artifact_from_region_extrude() {
14385 let initial_source = "\
14386s = sketch(on = YZ) {
14387 line1 = line(start = [0, 0], end = [0, 1])
14388 line2 = line(start = [0, 1], end = [1, 1])
14389 line3 = line(start = [1, 1], end = [0, 0])
14390}
14391region001 = region(point = [0.1, 0.1], sketch = s)
14392extrude001 = extrude(region001, length = 5)
14393";
14394
14395 let program = Program::parse(initial_source).unwrap().0.unwrap();
14396
14397 let mut frontend = FrontendState::new();
14398 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
14399 let version = Version(0);
14400
14401 frontend.hack_set_program(&ctx, program).await.unwrap();
14402 let wall_object_id = find_first_wall_object_id(&frontend.scene_graph).expect("expected a wall object");
14403
14404 let sketch_args = SketchCtor {
14405 on: Plane::Object(wall_object_id),
14406 };
14407 let (src_delta, _scene_delta, _sketch_id) = frontend
14408 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14409 .await
14410 .unwrap();
14411 assert!(src_delta.text.contains("faceOf(extrude001, face = region001.tags."));
14412
14413 ctx.close().await;
14414 }
14415
14416 #[tokio::test(flavor = "multi_thread")]
14417 async fn test_sketch_on_wall_artifact_from_split_region_extrude() {
14418 let initial_source = "\
14419sketch001 = sketch(on = YZ) {
14420 line1 = line(start = [var 0.49, var -0.39], end = [var 6.52, var -0.39])
14421 line2 = line(start = [var 6.52, var -0.39], end = [var 6.52, var 4.9])
14422 line3 = line(start = [var 6.52, var 4.9], end = [var 0.49, var 4.9])
14423 line4 = line(start = [var 0.49, var 4.9], end = [var 0.49, var -0.39])
14424 coincident([line1.end, line2.start])
14425 coincident([line2.end, line3.start])
14426 coincident([line3.end, line4.start])
14427 coincident([line4.end, line1.start])
14428 parallel([line2, line4])
14429 parallel([line3, line1])
14430 perpendicular([line1, line2])
14431 horizontal(line3)
14432 line5 = line(start = [2.35, 6.65], end = [5.89, -2.7])
14433}
14434region001 = region(point = [3.1, 3.74], sketch = sketch001)
14435extrude001 = extrude(region001, length = 5)
14436";
14437
14438 let program = Program::parse(initial_source).unwrap().0.unwrap();
14439
14440 let mut frontend = FrontendState::new();
14441 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
14442 let version = Version(0);
14443
14444 frontend.hack_set_program(&ctx, program).await.unwrap();
14445 let wall_object_id = find_first_wall_object_id(&frontend.scene_graph).expect("expected a wall object");
14446
14447 let sketch_args = SketchCtor {
14448 on: Plane::Object(wall_object_id),
14449 };
14450 let (src_delta, _scene_delta, _sketch_id) = frontend
14451 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14452 .await
14453 .unwrap();
14454 assert!(src_delta.text.contains("faceOf(extrude001, face = region001.tags."));
14455
14456 ctx.close().await;
14457 }
14458
14459 #[tokio::test(flavor = "multi_thread")]
14460 async fn test_new_sketch_on_multi_region_extrude_cap_indexes_selected_solid() {
14461 let initial_source = "\
14462@settings(kclVersion = 2.0)
14463
14464sketch001 = sketch(on = XY) {
14465 circle1 = circle(start = [var -1.51mm, var 1.31mm], center = [var -1.98mm, var 1.03mm])
14466 circle2 = circle(start = [var 2.98mm, var 2.37mm], center = [var 2.66mm, var 1.46mm])
14467}
14468hidden001 = hide(sketch001)
14469region001 = region(segments = [sketch001.circle2])
14470region002 = region(segments = [sketch001.circle1])
14471extrude001 = extrude([region001, region002], length = 5)
14472";
14473
14474 let program = Program::parse(initial_source).unwrap().0.unwrap();
14475 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
14476 let version = Version(0);
14477
14478 for (solid_output_index, expected_face) in [
14479 (0, "faceOf(extrude001[0], face = END)"),
14480 (1, "faceOf(extrude001[1], face = END)"),
14481 ] {
14482 let mut frontend = FrontendState::new();
14483 frontend.hack_set_program(&ctx, program.clone()).await.unwrap();
14484 let cap_object_id = find_cap_object_id_with_solid_output_index(
14485 &frontend.scene_graph,
14486 kcl_api::CapKind::End,
14487 solid_output_index,
14488 )
14489 .unwrap_or_else(|| panic!("expected an end cap object for solid output index {solid_output_index}"));
14490
14491 let sketch_args = SketchCtor {
14492 on: Plane::Object(cap_object_id),
14493 };
14494 let (src_delta, _scene_delta, _sketch_id) = frontend
14495 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14496 .await
14497 .unwrap();
14498
14499 assert!(
14500 src_delta.text.contains(expected_face),
14501 "expected `{expected_face}` in:\n{}",
14502 src_delta.text
14503 );
14504 assert!(!src_delta.text.contains("faceOf(extrude001, face = END)"));
14505 }
14506
14507 ctx.close().await;
14508 }
14509
14510 #[tokio::test(flavor = "multi_thread")]
14511 async fn test_new_sketch_on_multi_region_extrude_wall_indexes_selected_solid() {
14512 let initial_source = "\
14513@settings(kclVersion = 2.0)
14514
14515sketch001 = sketch(on = XY) {
14516 rect1Line1 = line(start = [0, 0], end = [1, 0])
14517 rect1Line2 = line(start = [1, 0], end = [1, 1])
14518 rect1Line3 = line(start = [1, 1], end = [0, 1])
14519 rect1Line4 = line(start = [0, 1], end = [0, 0])
14520 rect2Line1 = line(start = [3, 0], end = [4, 0])
14521 rect2Line2 = line(start = [4, 0], end = [4, 1])
14522 rect2Line3 = line(start = [4, 1], end = [3, 1])
14523 rect2Line4 = line(start = [3, 1], end = [3, 0])
14524}
14525hidden001 = hide(sketch001)
14526region001 = region(segments = [
14527 sketch001.rect1Line4,
14528 sketch001.rect1Line1
14529])
14530region002 = region(segments = [
14531 sketch001.rect2Line4,
14532 sketch001.rect2Line1
14533])
14534extrude001 = extrude([region001, region002], length = 5)
14535";
14536
14537 let program = Program::parse(initial_source).unwrap().0.unwrap();
14538 let mut frontend = FrontendState::new();
14539 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
14540 let version = Version(0);
14541
14542 frontend.hack_set_program(&ctx, program).await.unwrap();
14543 let region_call = "\
14544region(segments = [
14545 sketch001.rect1Line4,
14546 sketch001.rect1Line1
14547])";
14548 let region_call_start = initial_source.find(region_call).unwrap();
14549 let region_range = [region_call_start, region_call_start + region_call.len(), 0].into();
14550 let segment_call = "line(start = [0, 0], end = [1, 0])";
14551 let segment_call_start = initial_source.find(segment_call).unwrap();
14552 let segment_range = [segment_call_start, segment_call_start + segment_call.len(), 0].into();
14553 let wall_object_id = frontend
14554 .scene_graph
14555 .objects
14556 .iter()
14557 .find_map(|object| match &object.kind {
14558 ObjectKind::Wall(wall)
14559 if wall.source.path.as_ref().is_some_and(|path| path.range == region_range)
14560 && wall.source.segment.range == segment_range =>
14561 {
14562 Some(object.id)
14563 }
14564 _ => None,
14565 })
14566 .expect("expected a wall object for region001.tags.rect1Line1");
14567
14568 let sketch_args = SketchCtor {
14569 on: Plane::Object(wall_object_id),
14570 };
14571 let (src_delta, _scene_delta, _sketch_id) = frontend
14572 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14573 .await
14574 .unwrap();
14575
14576 let expected_face = "faceOf(extrude001[0], face = region001.tags.rect1Line1)";
14577 assert!(
14578 src_delta.text.contains(expected_face),
14579 "expected `{expected_face}` in:\n{}",
14580 src_delta.text
14581 );
14582 assert!(!src_delta.text.contains("faceOf(extrude001, face ="));
14583
14584 ctx.close().await;
14585 }
14586
14587 #[test]
14588 fn test_enclosing_variable_fallback_skips_nested_sketch_items() {
14589 let source = "\
14590sketch001 = sketch(on = XY) {
14591 line(start = [0, 0], end = [1, 0])
14592}
14593part = subtract(boxSolid, tools = [cutSolid])
14594 |> appearance(color = \"#8f96a3\")
14595";
14596 let ast = Program::parse(source).unwrap().0.unwrap().ast;
14597 let line_start = source.find("line").unwrap();
14598 let line_end = line_start + "line(start = [0, 0], end = [1, 0])".len();
14599 let line_ref = SourceRef::Simple {
14600 range: [line_start, line_end, 0].into(),
14601 node_path: None,
14602 };
14603 assert_eq!(variable_name_containing_source_ref(&ast, &line_ref), None);
14604
14605 let subtract_start = source.find("subtract").unwrap();
14606 let subtract_end = subtract_start + "subtract(boxSolid, tools = [cutSolid])".len();
14607 let subtract_ref = SourceRef::Simple {
14608 range: [subtract_start, subtract_end, 0].into(),
14609 node_path: None,
14610 };
14611 assert_eq!(
14612 variable_name_containing_source_ref(&ast, &subtract_ref),
14613 Some("part".to_owned())
14614 );
14615 }
14616
14617 #[tokio::test(flavor = "multi_thread")]
14618 async fn test_sketch_on_subtracted_sweep_cap_uses_composite_solid() {
14619 let source = "\
14620boxSolid = startSketchOn(XY)
14621 |> startProfile(at = [0, 0])
14622 |> line(end = [4, 0], tag = $bottomEdge)
14623 |> line(end = [0, 4])
14624 |> line(end = [-4, 0])
14625 |> close()
14626 |> extrude(length = 10)
14627cutSolid = startSketchOn(XY)
14628 |> startProfile(at = [1, 1])
14629 |> line(end = [1, 0])
14630 |> line(end = [0, 1])
14631 |> line(end = [-1, 0])
14632 |> close()
14633 |> extrude(length = 10)
14634part = subtract(boxSolid, tools = [cutSolid])
14635 |> appearance(color = \"#8f96a3\", roughness = 55, metalness = 8)
14636";
14637 let program = Program::parse(source).unwrap().0.unwrap();
14638 let mut frontend = FrontendState::new();
14639 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
14640 match frontend.hack_set_program(&ctx, program).await.unwrap() {
14641 SetProgramOutcome::Success { .. } => {}
14642 SetProgramOutcome::ExecFailure { error } => panic!("KCL fixture failed to execute: {error:?}"),
14643 }
14644
14645 let sweep_call_start = source.find("extrude").unwrap();
14646 let sweep_call_end = sweep_call_start + "extrude(length = 10)".len();
14647 let part_call_start = source.find("subtract").unwrap();
14648 let part_call_end = part_call_start + "subtract(boxSolid, tools = [cutSolid])".len();
14649 let sweep_range = [sweep_call_start, sweep_call_end, 0].into();
14650 let composite_range = [part_call_start, part_call_end, 0].into();
14651
14652 let cap_object = frontend
14653 .scene_graph
14654 .objects
14655 .iter()
14656 .find(|object| {
14657 matches!(
14658 &object.kind,
14659 ObjectKind::Cap(kcl_api::Cap {
14660 kind: kcl_api::CapKind::End,
14661 source,
14662 ..
14663 }) if source.solid.range == composite_range && source.sweep.range == sweep_range
14664 )
14665 })
14666 .expect("expected end cap object to trace through subtract and original extrude");
14667
14668 let mut ast = frontend.program.ast.clone();
14669 let cap_expr = sketch_on_ast_expr(
14670 &mut ast,
14671 &frontend.scene_graph,
14672 &frontend.solid_references,
14673 &Plane::Object(cap_object.id),
14674 )
14675 .unwrap();
14676 let cap_face_decl = ast::VariableDeclaration::new(
14677 ast::VariableDeclarator::new("capFace", cap_expr.clone()),
14678 ast::ItemVisibility::Default,
14679 ast::VariableKind::Const,
14680 );
14681 ast.body
14682 .push(ast::BodyItem::VariableDeclaration(BoxNode::new(ast::Node::no_src(
14683 cap_face_decl,
14684 ))));
14685 let generated_source = source_from_ast(&ast);
14686
14687 assert!(generated_source.contains("capFace = faceOf(part, face = END)"));
14688 assert!(!generated_source.contains("faceOf(boxSolid"));
14689 let ast::Expr::CallExpressionKw(call) = cap_expr else {
14690 panic!("expected faceOf call");
14691 };
14692 assert_eq!(call.callee.name.name, "faceOf");
14693 let ast::Expr::Name(solid_name) = call.unlabeled.as_ref().unwrap() else {
14694 panic!("expected solid name");
14695 };
14696 assert_eq!(solid_name.name.name, "part");
14697 let ast::Expr::Name(face_name) = &call.arguments[0].arg else {
14698 panic!("expected face name");
14699 };
14700 assert_eq!(face_name.name.name, "END");
14701
14702 ctx.close().await;
14703 }
14704
14705 #[tokio::test(flavor = "multi_thread")]
14706 async fn test_sketch_on_plane_incremental() {
14707 let initial_source = "\
14708len = 2mm
14709cube = startSketchOn(XY)
14710 |> startProfile(at = [0, 0])
14711 |> line(end = [len, 0], tag = $side)
14712 |> line(end = [0, len])
14713 |> line(end = [-len, 0])
14714 |> line(end = [0, -len])
14715 |> close()
14716 |> extrude(length = len)
14717
14718plane = planeOf(cube, face = side)
14719";
14720
14721 let program = Program::parse(initial_source).unwrap().0.unwrap();
14722
14723 let mut frontend = FrontendState::new();
14724
14725 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
14726 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
14727 let version = Version(0);
14728
14729 frontend.hack_set_program(&ctx, program).await.unwrap();
14730 let plane_object = frontend
14732 .scene_graph
14733 .objects
14734 .iter()
14735 .rev()
14736 .find(|object| matches!(&object.kind, ObjectKind::Plane(_)))
14737 .unwrap();
14738 let plane_id = plane_object.id;
14739
14740 let sketch_args = SketchCtor {
14741 on: Plane::Object(plane_id),
14742 };
14743 let (src_delta, scene_delta, sketch_id) = frontend
14744 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14745 .await
14746 .unwrap();
14747 insta::assert_snapshot!("test_sketch_on_plane_incremental", src_delta.text.as_str());
14748 assert_eq!(sketch_id, ObjectId(2));
14749 assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
14750 let sketch_object = &scene_delta.new_graph.objects[2];
14751 assert_eq!(sketch_object.id, ObjectId(2));
14752 assert_eq!(
14753 sketch_object.kind,
14754 ObjectKind::Sketch(Sketch {
14755 args: SketchCtor {
14756 on: Plane::Object(plane_id),
14757 },
14758 plane: plane_id,
14759 segments: vec![],
14760 constraints: vec![],
14761 })
14762 );
14763 assert_eq!(scene_delta.new_graph.objects.len(), 9);
14764
14765 let plane_object = scene_delta.new_graph.objects.get(plane_id.0).unwrap();
14766 assert_eq!(plane_object.id, plane_id);
14767 assert_eq!(plane_object.kind, ObjectKind::Plane(Plane::Object(plane_id)));
14768
14769 ctx.close().await;
14770 mock_ctx.close().await;
14771 }
14772
14773 #[tokio::test(flavor = "multi_thread")]
14774 async fn test_new_sketch_uses_unique_variable_name() {
14775 let initial_source = "\
14776sketch1 = sketch(on = XY) {
14777}
14778";
14779
14780 let program = Program::parse(initial_source).unwrap().0.unwrap();
14781
14782 let mut frontend = FrontendState::new();
14783 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
14784 let version = Version(0);
14785
14786 frontend.hack_set_program(&ctx, program).await.unwrap();
14787
14788 let sketch_args = SketchCtor {
14789 on: Plane::Default(PlaneName::Yz),
14790 };
14791 let (src_delta, _, _) = frontend
14792 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14793 .await
14794 .unwrap();
14795
14796 insta::assert_snapshot!("test_new_sketch_uses_unique_variable_name", src_delta.text.as_str());
14797
14798 ctx.close().await;
14799 }
14800
14801 #[tokio::test(flavor = "multi_thread")]
14802 async fn test_new_sketch_twice_using_same_plane() {
14803 let initial_source = "\
14804sketch1 = sketch(on = XY) {
14805}
14806";
14807
14808 let program = Program::parse(initial_source).unwrap().0.unwrap();
14809
14810 let mut frontend = FrontendState::new();
14811 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
14812 let version = Version(0);
14813
14814 frontend.hack_set_program(&ctx, program).await.unwrap();
14815
14816 let sketch_args = SketchCtor {
14817 on: Plane::Default(PlaneName::Xy),
14818 };
14819 let (src_delta, _, _) = frontend
14820 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14821 .await
14822 .unwrap();
14823
14824 insta::assert_snapshot!("test_new_sketch_twice_using_same_plane", src_delta.text.as_str());
14825
14826 ctx.close().await;
14827 }
14828
14829 #[tokio::test(flavor = "multi_thread")]
14830 async fn test_sketch_mode_reuses_cached_on_expression() {
14831 let initial_source = "\
14832width = 2mm
14833sketch(on = offsetPlane(XY, offset = width)) {
14834 line1 = line(start = [var 0, var 0], end = [var 1mm, var 0])
14835 distance([line1.start, line1.end]) == width
14836}
14837";
14838 let program = Program::parse(initial_source).unwrap().0.unwrap();
14839
14840 let mut frontend = FrontendState::new();
14841 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
14842 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
14843 let version = Version(0);
14844 let project_id = ProjectId(0);
14845 let file_id = FileId(0);
14846
14847 frontend.hack_set_program(&ctx, program).await.unwrap();
14848 let initial_object_count = frontend.scene_graph.objects.len();
14849 let sketch_id = find_first_sketch_object(&frontend.scene_graph)
14850 .expect("Expected sketch object to exist")
14851 .id;
14852
14853 let scene_delta = frontend
14856 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
14857 .await
14858 .unwrap();
14859 assert_eq!(scene_delta.new_graph.objects.len(), initial_object_count);
14860
14861 let (_src_delta, scene_delta) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
14864 assert_eq!(scene_delta.new_graph.objects.len(), initial_object_count);
14865
14866 ctx.close().await;
14867 mock_ctx.close().await;
14868 }
14869
14870 #[tokio::test(flavor = "multi_thread")]
14871 async fn test_edit_sketch_nested_in_pipe() {
14872 let source = r#"
14873profile = sketch(on = XY) {
14874 line1 = line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])
14875}
14876 |> translate(x = 2mm)
14877"#;
14878 let program = Program::parse_no_errs(source).unwrap();
14879 let mut frontend = FrontendState::new();
14880 let mock_ctx = ExecutorContext::new_mock(None).await;
14881 let version = Version(0);
14882
14883 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
14884 let sketch_id = find_first_sketch_object(&frontend.scene_graph)
14885 .expect("Expected piped sketch object")
14886 .id;
14887
14888 let scene_delta = frontend
14889 .edit_sketch(&mock_ctx, ProjectId(0), FileId(0), version, sketch_id)
14890 .await
14891 .unwrap();
14892 assert_eq!(scene_delta.new_graph.sketch_mode, Some(sketch_id));
14893 assert!(
14894 scene_delta
14895 .new_graph
14896 .objects
14897 .iter()
14898 .any(|object| matches!(&object.kind, ObjectKind::Segment { .. })),
14899 "Expected the piped sketch's segments to be present in sketch mode"
14900 );
14901
14902 mock_ctx.cache.clear_mem_cache().await;
14903 mock_ctx.close().await;
14904 }
14905
14906 #[tokio::test(flavor = "multi_thread")]
14907 async fn test_issue_9409_edit_sketch_nested_in_if_with_var_feedback() {
14908 let source = r#"
14909useFirstProfile = true
14910
14911profile = if useFirstProfile {
14912 sketch(on = XY) {
14913 line1 = line(start = [0mm, 0mm], end = [var 20mm, var 10mm])
14914 }
14915} else {
14916 sketch(on = XY) {
14917 line2 = line(start = [0mm, 0mm], end = [var 10mm, var 20mm])
14918 }
14919}
14920"#;
14921 let program = Program::parse_no_errs(source).unwrap();
14922 let mut frontend = FrontendState::new();
14923 let mock_ctx = ExecutorContext::new_mock(None).await;
14924 let version = Version(0);
14925
14926 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
14927 let sketch_object =
14928 find_first_sketch_object(&frontend.scene_graph).expect("Expected active branch's sketch object");
14929 let sketch_id = sketch_object.id;
14930 let sketch = expect_sketch(sketch_object);
14931 let line_end_id = *sketch
14932 .segments
14933 .get(1)
14934 .expect("Expected the active branch's line end point");
14935
14936 let scene_delta = frontend
14937 .edit_sketch(&mock_ctx, ProjectId(0), FileId(0), version, sketch_id)
14938 .await
14939 .unwrap();
14940 assert_eq!(scene_delta.new_graph.sketch_mode, Some(sketch_id));
14941
14942 let segments = vec![ExistingSegmentCtor {
14943 id: line_end_id,
14944 ctor: SegmentCtor::Point(PointCtor {
14945 position: Point2d {
14946 x: Expr::Var(Number {
14947 value: 30.0,
14948 units: NumericSuffix::Mm,
14949 }),
14950 y: Expr::Var(Number {
14951 value: 15.0,
14952 units: NumericSuffix::Mm,
14953 }),
14954 },
14955 }),
14956 }];
14957 let (source_delta, _) = frontend
14958 .edit_segments(&mock_ctx, version, sketch_id, segments)
14959 .await
14960 .unwrap();
14961 assert!(
14962 source_delta
14963 .text
14964 .contains("line1 = line(start = [0mm, 0mm], end = [var 30mm, var 15mm])"),
14965 "Expected the active branch's dragged variables to be updated:\n{}",
14966 source_delta.text
14967 );
14968 assert!(
14969 source_delta
14970 .text
14971 .contains("line2 = line(start = [0mm, 0mm], end = [var 10mm, var 20mm])"),
14972 "Expected the inactive branch to remain unchanged:\n{}",
14973 source_delta.text
14974 );
14975
14976 mock_ctx.cache.clear_mem_cache().await;
14977 mock_ctx.close().await;
14978 }
14979
14980 #[tokio::test(flavor = "multi_thread")]
14981 async fn test_multiple_sketch_blocks() {
14982 let initial_source = "\
14983// Cube that requires the engine.
14984width = 2
14985sketch001 = startSketchOn(XY)
14986profile001 = startProfile(sketch001, at = [0, 0])
14987 |> yLine(length = width, tag = $seg1)
14988 |> xLine(length = width)
14989 |> yLine(length = -width)
14990 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
14991 |> close()
14992extrude001 = extrude(profile001, length = width)
14993
14994// Get a value that requires the engine.
14995x = segLen(seg1)
14996
14997// Triangle with side length 2*x.
14998sketch(on = XY) {
14999 line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
15000 line2 = line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
15001 coincident([line1.end, line2.start])
15002 line3 = line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
15003 coincident([line2.end, line3.start])
15004 coincident([line3.end, line1.start])
15005 equalLength([line3, line1])
15006 equalLength([line1, line2])
15007 distance([line1.start, line1.end]) == 2*x
15008}
15009
15010// Line segment with length x.
15011sketch2 = sketch(on = XY) {
15012 line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
15013 distance([line1.start, line1.end]) == x
15014}
15015";
15016
15017 let program = Program::parse(initial_source).unwrap().0.unwrap();
15018
15019 let mut frontend = FrontendState::new();
15020
15021 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
15022 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
15023 let version = Version(0);
15024 let project_id = ProjectId(0);
15025 let file_id = FileId(0);
15026
15027 frontend.hack_set_program(&ctx, program).await.unwrap();
15028 let sketch_objects = frontend
15029 .scene_graph
15030 .objects
15031 .iter()
15032 .filter(|obj| matches!(obj.kind, ObjectKind::Sketch(_)))
15033 .collect::<Vec<_>>();
15034 let sketch1_id = sketch_objects.first().unwrap().id;
15035 let sketch2_id = sketch_objects.get(1).unwrap().id;
15036 let point1_id = ObjectId(sketch1_id.0 + 1);
15038 let point2_id = ObjectId(sketch2_id.0 + 1);
15040
15041 let scene_delta = frontend
15050 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch1_id)
15051 .await
15052 .unwrap();
15053 assert_eq!(
15054 scene_delta.new_graph.objects.len(),
15055 18,
15056 "{:#?}",
15057 scene_delta.new_graph.objects
15058 );
15059
15060 let point_ctor = PointCtor {
15062 position: Point2d {
15063 x: Expr::Var(Number {
15064 value: 1.0,
15065 units: NumericSuffix::Mm,
15066 }),
15067 y: Expr::Var(Number {
15068 value: 2.0,
15069 units: NumericSuffix::Mm,
15070 }),
15071 },
15072 };
15073 let segments = vec![ExistingSegmentCtor {
15074 id: point1_id,
15075 ctor: SegmentCtor::Point(point_ctor),
15076 }];
15077 let (src_delta, _) = frontend
15078 .edit_segments(&mock_ctx, version, sketch1_id, segments)
15079 .await
15080 .unwrap();
15081 insta::assert_snapshot!("test_multiple_sketch_blocks_1", src_delta.text.as_str());
15083 let edited_sketch1_source = src_delta.text.clone();
15084
15085 let (src_delta, _) = frontend.execute_mock(&mock_ctx, version, sketch1_id).await.unwrap();
15087 assert_eq!(src_delta.text, edited_sketch1_source);
15088 let scene = frontend.exit_sketch(&ctx, version, sketch1_id).await.unwrap();
15096 assert_eq!(scene.objects.len(), 30, "{:#?}", scene.objects);
15097
15098 let scene_delta = frontend
15106 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch2_id)
15107 .await
15108 .unwrap();
15109 assert_eq!(
15110 scene_delta.new_graph.objects.len(),
15111 24,
15112 "{:#?}",
15113 scene_delta.new_graph.objects
15114 );
15115
15116 let point_ctor = PointCtor {
15118 position: Point2d {
15119 x: Expr::Var(Number {
15120 value: 3.0,
15121 units: NumericSuffix::Mm,
15122 }),
15123 y: Expr::Var(Number {
15124 value: 4.0,
15125 units: NumericSuffix::Mm,
15126 }),
15127 },
15128 };
15129 let segments = vec![ExistingSegmentCtor {
15130 id: point2_id,
15131 ctor: SegmentCtor::Point(point_ctor),
15132 }];
15133 let (src_delta, _) = frontend
15134 .edit_segments(&mock_ctx, version, sketch2_id, segments)
15135 .await
15136 .unwrap();
15137 insta::assert_snapshot!("test_multiple_sketch_blocks_2", src_delta.text.as_str());
15139 let edited_sketch2_source = src_delta.text.clone();
15140
15141 let (src_delta, _) = frontend.execute_mock(&mock_ctx, version, sketch2_id).await.unwrap();
15143 assert_eq!(src_delta.text, edited_sketch2_source);
15144
15145 ctx.close().await;
15146 mock_ctx.close().await;
15147 }
15148
15149 #[tokio::test(flavor = "multi_thread")]
15150 async fn test_exit_sketch_without_changes_allows_entering_next_sketch() {
15151 let source = r#"sketch001 = sketch(on = XZ) {
15152 circle1 = circle(start = [var -1.96mm, var 2.77mm], center = [var -2.69mm, var 3.44mm])
15153}
15154sketch002 = sketch(on = XY) {
15155 line1 = line(start = [var 0mm, var 0mm], end = [var 4.68mm, var 0mm])
15156 line2 = line(start = [var 4.68mm, var 0mm], end = [var 4.68mm, var 2.96mm])
15157 line3 = line(start = [var 4.68mm, var 2.96mm], end = [var 0mm, var 2.96mm])
15158 line4 = line(start = [var 0mm, var 2.96mm], end = [var 0mm, var 0mm])
15159 coincident([line1.end, line2.start])
15160 coincident([line2.end, line3.start])
15161 coincident([line3.end, line4.start])
15162 coincident([line4.end, line1.start])
15163 parallel([line2, line4])
15164 parallel([line3, line1])
15165 perpendicular([line1, line2])
15166 horizontal(line3)
15167 coincident([line1.start, ORIGIN])
15168}
15169"#;
15170
15171 let program = Program::parse(source).unwrap().0.unwrap();
15172 let mut frontend = FrontendState::new();
15173 let ctx = ExecutorContext::new_with_engine(sync::Arc::new(EngineManager::new_mock()), Default::default());
15174 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
15175 let version = Version(0);
15176 let project_id = ProjectId(0);
15177 let file_id = FileId(0);
15178
15179 frontend.hack_set_program(&ctx, program).await.unwrap();
15180 let sketch_objects = frontend
15181 .scene_graph
15182 .objects
15183 .iter()
15184 .filter(|object| matches!(object.kind, ObjectKind::Sketch(_)))
15185 .collect::<Vec<_>>();
15186 assert_eq!(sketch_objects.len(), 2, "{:#?}", frontend.scene_graph.objects);
15187
15188 let sketch1_id = sketch_objects[0].id;
15189 let sketch2_id = sketch_objects[1].id;
15190
15191 frontend
15192 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch1_id)
15193 .await
15194 .unwrap();
15195 frontend.exit_sketch(&ctx, version, sketch1_id).await.unwrap();
15196
15197 let scene_delta = frontend
15198 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch2_id)
15199 .await
15200 .unwrap();
15201 assert_eq!(scene_delta.new_graph.sketch_mode, Some(sketch2_id));
15202
15203 ctx.cache.clear_mem_cache().await;
15204 ctx.close().await;
15205 mock_ctx.close().await;
15206 }
15207
15208 #[tokio::test(flavor = "multi_thread")]
15213 async fn test_extra_newlines_after_settings_edit_sketch_add_point() {
15214 let initial_source = "@settings(defaultLengthUnit = mm)
15216
15217sketch001 = sketch(on = XY) {
15218 point(at = [1in, 2in])
15219}
15220";
15221
15222 let program = Program::parse(initial_source).unwrap().0.unwrap();
15223 let mut frontend = FrontendState::new();
15224
15225 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
15226 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
15227 let version = Version(0);
15228 let project_id = ProjectId(0);
15229 let file_id = FileId(0);
15230
15231 frontend.hack_set_program(&ctx, program).await.unwrap();
15232 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15233 let sketch_id = sketch_object.id;
15234
15235 frontend
15237 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
15238 .await
15239 .unwrap();
15240
15241 let point_ctor = PointCtor {
15243 position: Point2d {
15244 x: Expr::Number(Number {
15245 value: 5.0,
15246 units: NumericSuffix::Mm,
15247 }),
15248 y: Expr::Number(Number {
15249 value: 6.0,
15250 units: NumericSuffix::Mm,
15251 }),
15252 },
15253 };
15254 let segment = SegmentCtor::Point(point_ctor);
15255 let (src_delta, scene_delta) = frontend
15256 .add_segment(&mock_ctx, version, sketch_id, segment, None)
15257 .await
15258 .unwrap();
15259 assert!(
15261 src_delta.text.contains("point(at = [5mm, 6mm])"),
15262 "Expected new point in source, got: {}",
15263 src_delta.text
15264 );
15265 assert!(!scene_delta.new_objects.is_empty());
15266
15267 ctx.close().await;
15268 mock_ctx.close().await;
15269 }
15270
15271 #[tokio::test(flavor = "multi_thread")]
15272 async fn test_ensure_control_point_spline_experimental_features_adds_allow_setting() {
15273 let initial_program = Program::parse("s = sketch(on = XY) {}\n").unwrap().0.unwrap();
15274
15275 let updated_program = ensure_control_point_spline_experimental_features(&initial_program).unwrap();
15276 let meta_settings = updated_program.meta_settings().unwrap().unwrap();
15277
15278 assert_eq!(meta_settings.experimental_features, WarningLevel::Allow);
15279 assert!(
15280 source_from_ast(&updated_program.ast).contains("@settings(experimentalFeatures = allow)"),
15281 "Expected experimental settings to be added to source"
15282 );
15283 }
15284
15285 #[tokio::test(flavor = "multi_thread")]
15286 async fn test_extra_newlines_after_settings_add_line_to_empty_sketch() {
15287 let initial_source = "@settings(defaultLengthUnit = mm)
15289
15290s = sketch(on = XY) {}
15291";
15292
15293 let program = Program::parse(initial_source).unwrap().0.unwrap();
15294 let mut frontend = FrontendState::new();
15295
15296 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
15297 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
15298 let version = Version(0);
15299
15300 frontend.hack_set_program(&ctx, program).await.unwrap();
15301 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15302 let sketch_id = sketch_object.id;
15303
15304 let line_ctor = LineCtor {
15305 start: Point2d {
15306 x: Expr::Number(Number {
15307 value: 0.0,
15308 units: NumericSuffix::Mm,
15309 }),
15310 y: Expr::Number(Number {
15311 value: 0.0,
15312 units: NumericSuffix::Mm,
15313 }),
15314 },
15315 end: Point2d {
15316 x: Expr::Number(Number {
15317 value: 10.0,
15318 units: NumericSuffix::Mm,
15319 }),
15320 y: Expr::Number(Number {
15321 value: 10.0,
15322 units: NumericSuffix::Mm,
15323 }),
15324 },
15325 construction: None,
15326 };
15327 let segment = SegmentCtor::Line(line_ctor);
15328 let (src_delta, scene_delta) = frontend
15329 .add_segment(&mock_ctx, version, sketch_id, segment, None)
15330 .await
15331 .unwrap();
15332 assert!(
15333 src_delta.text.contains("line(start = [0mm, 0mm], end = [10mm, 10mm])"),
15334 "Expected line in source, got: {}",
15335 src_delta.text
15336 );
15337 assert_eq!(scene_delta.new_objects.len(), 3);
15339
15340 ctx.close().await;
15341 mock_ctx.close().await;
15342 }
15343
15344 #[tokio::test(flavor = "multi_thread")]
15345 async fn test_extra_newlines_between_operations_edit_line() {
15346 let initial_source = "@settings(defaultLengthUnit = mm)
15348
15349sketch001 = sketch(on = XY) {
15350
15351 line1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm])
15352
15353}
15354";
15355
15356 let program = Program::parse(initial_source).unwrap().0.unwrap();
15357 let mut frontend = FrontendState::new();
15358
15359 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
15360 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
15361 let version = Version(0);
15362 let project_id = ProjectId(0);
15363 let file_id = FileId(0);
15364
15365 let outcome = frontend.hack_set_program(&ctx, program).await.unwrap();
15366 assert!(matches!(outcome, SetProgramOutcome::Success { .. }), "{outcome:?}");
15367 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15368 let sketch_id = sketch_object.id;
15369 let sketch = expect_sketch(sketch_object);
15370
15371 let line_id = sketch
15373 .segments
15374 .iter()
15375 .copied()
15376 .find(|seg_id| {
15377 matches!(
15378 &frontend.scene_graph.objects[seg_id.0].kind,
15379 ObjectKind::Segment {
15380 segment: Segment::Line(_)
15381 }
15382 )
15383 })
15384 .expect("Expected a line segment in sketch");
15385
15386 frontend
15388 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
15389 .await
15390 .unwrap();
15391
15392 let line_ctor = LineCtor {
15394 start: Point2d {
15395 x: Expr::Var(Number {
15396 value: 1.0,
15397 units: NumericSuffix::Mm,
15398 }),
15399 y: Expr::Var(Number {
15400 value: 2.0,
15401 units: NumericSuffix::Mm,
15402 }),
15403 },
15404 end: Point2d {
15405 x: Expr::Var(Number {
15406 value: 13.0,
15407 units: NumericSuffix::Mm,
15408 }),
15409 y: Expr::Var(Number {
15410 value: 14.0,
15411 units: NumericSuffix::Mm,
15412 }),
15413 },
15414 construction: None,
15415 };
15416 let segments = vec![ExistingSegmentCtor {
15417 id: line_id,
15418 ctor: SegmentCtor::Line(line_ctor),
15419 }];
15420 let (src_delta, _scene_delta) = frontend
15421 .edit_segments(&mock_ctx, version, sketch_id, segments)
15422 .await
15423 .unwrap();
15424 assert!(
15425 src_delta
15426 .text
15427 .contains("line(start = [var 1mm, var 2mm], end = [var 13mm, var 14mm])"),
15428 "Expected edited line in source, got: {}",
15429 src_delta.text
15430 );
15431
15432 ctx.close().await;
15433 mock_ctx.close().await;
15434 }
15435
15436 #[tokio::test(flavor = "multi_thread")]
15437 async fn test_extra_newlines_delete_segment() {
15438 let initial_source = "@settings(defaultLengthUnit = mm)
15440
15441sketch001 = sketch(on = XY) {
15442 circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
15443}
15444";
15445
15446 let program = Program::parse(initial_source).unwrap().0.unwrap();
15447 let mut frontend = FrontendState::new();
15448
15449 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
15450 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
15451 let version = Version(0);
15452
15453 frontend.hack_set_program(&ctx, program).await.unwrap();
15454 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15455 let sketch_id = sketch_object.id;
15456 let sketch = expect_sketch(sketch_object);
15457
15458 assert_eq!(sketch.segments.len(), 3);
15460 let circle_id = sketch.segments[2];
15461
15462 let (src_delta, scene_delta) = frontend
15464 .delete_objects(&mock_ctx, version, sketch_id, vec![], vec![circle_id])
15465 .await
15466 .unwrap();
15467 assert!(
15468 src_delta.text.contains("sketch(on = XY) {"),
15469 "Expected sketch block in source, got: {}",
15470 src_delta.text
15471 );
15472 let new_sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
15473 let new_sketch = expect_sketch(new_sketch_object);
15474 assert_eq!(new_sketch.segments.len(), 0);
15475
15476 ctx.close().await;
15477 mock_ctx.close().await;
15478 }
15479
15480 #[tokio::test(flavor = "multi_thread")]
15481 async fn test_unformatted_source_add_arc() {
15482 let initial_source = "@settings(defaultLengthUnit = mm)
15484
15485sketch001 = sketch(on = XY) {
15486}
15487";
15488
15489 let program = Program::parse(initial_source).unwrap().0.unwrap();
15490 let mut frontend = FrontendState::new();
15491
15492 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
15493 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
15494 let version = Version(0);
15495
15496 frontend.hack_set_program(&ctx, program).await.unwrap();
15497 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15498 let sketch_id = sketch_object.id;
15499
15500 let arc_ctor = ArcCtor {
15501 start: Point2d {
15502 x: Expr::Var(Number {
15503 value: 5.0,
15504 units: NumericSuffix::Mm,
15505 }),
15506 y: Expr::Var(Number {
15507 value: 0.0,
15508 units: NumericSuffix::Mm,
15509 }),
15510 },
15511 end: Point2d {
15512 x: Expr::Var(Number {
15513 value: 0.0,
15514 units: NumericSuffix::Mm,
15515 }),
15516 y: Expr::Var(Number {
15517 value: 5.0,
15518 units: NumericSuffix::Mm,
15519 }),
15520 },
15521 center: Point2d {
15522 x: Expr::Var(Number {
15523 value: 0.0,
15524 units: NumericSuffix::Mm,
15525 }),
15526 y: Expr::Var(Number {
15527 value: 0.0,
15528 units: NumericSuffix::Mm,
15529 }),
15530 },
15531 direction: None,
15532 construction: None,
15533 };
15534 let segment = SegmentCtor::Arc(arc_ctor);
15535 let (src_delta, scene_delta) = frontend
15536 .add_segment(&mock_ctx, version, sketch_id, segment, None)
15537 .await
15538 .unwrap();
15539 assert!(
15540 src_delta
15541 .text
15542 .contains("arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])"),
15543 "Expected arc in source, got: {}",
15544 src_delta.text
15545 );
15546 assert!(!scene_delta.new_objects.is_empty());
15547
15548 ctx.close().await;
15549 mock_ctx.close().await;
15550 }
15551
15552 #[tokio::test(flavor = "multi_thread")]
15553 async fn test_arc_direction_flows_to_source() {
15554 let initial_source = "@settings(defaultLengthUnit = mm)
15555
15556sketch001 = sketch(on = XY) {
15557}
15558";
15559
15560 let program = Program::parse(initial_source).unwrap().0.unwrap();
15561 let mut frontend = FrontendState::new();
15562
15563 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
15564 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
15565 let version = Version(0);
15566
15567 frontend.hack_set_program(&ctx, program).await.unwrap();
15568 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15569 let sketch_id = sketch_object.id;
15570
15571 let point = |x: f64, y: f64| Point2d {
15572 x: Expr::Var(Number {
15573 value: x,
15574 units: NumericSuffix::Mm,
15575 }),
15576 y: Expr::Var(Number {
15577 value: y,
15578 units: NumericSuffix::Mm,
15579 }),
15580 };
15581
15582 let arc_ctor = ArcCtor {
15584 start: point(5.0, 0.0),
15585 end: point(0.0, 5.0),
15586 center: point(0.0, 0.0),
15587 direction: Some(ArcDirection::Cw),
15588 construction: None,
15589 };
15590 let (src_delta, scene_delta) = frontend
15591 .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Arc(arc_ctor), None)
15592 .await
15593 .unwrap();
15594 assert!(
15595 src_delta.text.contains("direction = CW"),
15596 "Expected direction = CW in source, got: {}",
15597 src_delta.text
15598 );
15599 let arc_id = *scene_delta.new_objects.last().unwrap();
15601
15602 let edited_ctor = ArcCtor {
15606 start: point(0.0, -5.0),
15607 end: point(0.0, 5.0),
15608 center: point(0.0, 0.0),
15609 direction: Some(ArcDirection::Cw),
15610 construction: None,
15611 };
15612 let (src_delta, _scene_delta) = frontend
15613 .edit_segments(
15614 &mock_ctx,
15615 version,
15616 sketch_id,
15617 vec![ExistingSegmentCtor {
15618 id: arc_id,
15619 ctor: SegmentCtor::Arc(edited_ctor),
15620 }],
15621 )
15622 .await
15623 .unwrap();
15624 assert!(
15625 src_delta.text.contains("start = [var 0mm, var -5mm]"),
15626 "Expected edited start point in source, got: {}",
15627 src_delta.text
15628 );
15629 assert!(
15630 src_delta.text.contains("direction = CW"),
15631 "Expected direction = CW to be preserved in source, got: {}",
15632 src_delta.text
15633 );
15634
15635 let edited_ctor = ArcCtor {
15638 start: point(0.0, -5.0),
15639 end: point(0.0, 5.0),
15640 center: point(0.0, 0.0),
15641 direction: Some(ArcDirection::Ccw),
15642 construction: None,
15643 };
15644 let (src_delta, _scene_delta) = frontend
15645 .edit_segments(
15646 &mock_ctx,
15647 version,
15648 sketch_id,
15649 vec![ExistingSegmentCtor {
15650 id: arc_id,
15651 ctor: SegmentCtor::Arc(edited_ctor),
15652 }],
15653 )
15654 .await
15655 .unwrap();
15656 assert!(
15657 !src_delta.text.contains("direction"),
15658 "Expected direction argument to be removed from source, got: {}",
15659 src_delta.text
15660 );
15661
15662 ctx.close().await;
15663 mock_ctx.close().await;
15664 }
15665
15666 #[tokio::test(flavor = "multi_thread")]
15667 async fn test_extra_newlines_add_circle() {
15668 let initial_source = "@settings(defaultLengthUnit = mm)
15670
15671sketch001 = sketch(on = XY) {
15672}
15673";
15674
15675 let program = Program::parse(initial_source).unwrap().0.unwrap();
15676 let mut frontend = FrontendState::new();
15677
15678 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
15679 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
15680 let version = Version(0);
15681
15682 frontend.hack_set_program(&ctx, program).await.unwrap();
15683 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15684 let sketch_id = sketch_object.id;
15685
15686 let circle_ctor = CircleCtor {
15687 start: Point2d {
15688 x: Expr::Var(Number {
15689 value: 5.0,
15690 units: NumericSuffix::Mm,
15691 }),
15692 y: Expr::Var(Number {
15693 value: 0.0,
15694 units: NumericSuffix::Mm,
15695 }),
15696 },
15697 center: Point2d {
15698 x: Expr::Var(Number {
15699 value: 0.0,
15700 units: NumericSuffix::Mm,
15701 }),
15702 y: Expr::Var(Number {
15703 value: 0.0,
15704 units: NumericSuffix::Mm,
15705 }),
15706 },
15707 construction: None,
15708 };
15709 let segment = SegmentCtor::Circle(circle_ctor);
15710 let (src_delta, scene_delta) = frontend
15711 .add_segment(&mock_ctx, version, sketch_id, segment, None)
15712 .await
15713 .unwrap();
15714 assert!(
15715 src_delta
15716 .text
15717 .contains("circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])"),
15718 "Expected circle in source, got: {}",
15719 src_delta.text
15720 );
15721 assert!(!scene_delta.new_objects.is_empty());
15722
15723 ctx.close().await;
15724 mock_ctx.close().await;
15725 }
15726
15727 #[tokio::test(flavor = "multi_thread")]
15728 async fn test_extra_newlines_add_constraint() {
15729 let initial_source = "@settings(defaultLengthUnit = mm)
15731
15732sketch001 = sketch(on = XY) {
15733 line1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm])
15734 line2 = line(start = [var 10mm, var 10mm], end = [var 20mm, var 0mm])
15735}
15736";
15737
15738 let program = Program::parse(initial_source).unwrap().0.unwrap();
15739 let mut frontend = FrontendState::new();
15740
15741 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
15742 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
15743 let version = Version(0);
15744 let project_id = ProjectId(0);
15745 let file_id = FileId(0);
15746
15747 frontend.hack_set_program(&ctx, program).await.unwrap();
15748 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15749 let sketch_id = sketch_object.id;
15750 let sketch = expect_sketch(sketch_object);
15751
15752 let line_ids: Vec<ObjectId> = sketch
15754 .segments
15755 .iter()
15756 .copied()
15757 .filter(|seg_id| {
15758 matches!(
15759 &frontend.scene_graph.objects[seg_id.0].kind,
15760 ObjectKind::Segment {
15761 segment: Segment::Line(_)
15762 }
15763 )
15764 })
15765 .collect();
15766 assert_eq!(line_ids.len(), 2, "Expected two line segments");
15767
15768 let line1 = &frontend.scene_graph.objects[line_ids[0].0];
15769 let ObjectKind::Segment {
15770 segment: Segment::Line(line1_data),
15771 } = &line1.kind
15772 else {
15773 panic!("Expected line");
15774 };
15775 let line2 = &frontend.scene_graph.objects[line_ids[1].0];
15776 let ObjectKind::Segment {
15777 segment: Segment::Line(line2_data),
15778 } = &line2.kind
15779 else {
15780 panic!("Expected line");
15781 };
15782
15783 let constraint = Constraint::Coincident(Coincident {
15785 segments: vec![line1_data.end.into(), line2_data.start.into()],
15786 });
15787
15788 frontend
15790 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
15791 .await
15792 .unwrap();
15793 let (src_delta, _scene_delta) = frontend
15794 .add_constraint(&mock_ctx, version, sketch_id, constraint)
15795 .await
15796 .unwrap();
15797 assert!(
15798 src_delta.text.contains("coincident("),
15799 "Expected coincident constraint in source, got: {}",
15800 src_delta.text
15801 );
15802
15803 ctx.close().await;
15804 mock_ctx.close().await;
15805 }
15806
15807 #[tokio::test(flavor = "multi_thread")]
15808 async fn test_extra_newlines_add_line_then_edit_line() {
15809 let initial_source = "@settings(defaultLengthUnit = mm)
15811
15812sketch001 = sketch(on = XY) {
15813}
15814";
15815
15816 let program = Program::parse(initial_source).unwrap().0.unwrap();
15817 let mut frontend = FrontendState::new();
15818
15819 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
15820 let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
15821 let version = Version(0);
15822
15823 frontend.hack_set_program(&ctx, program).await.unwrap();
15824 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15825 let sketch_id = sketch_object.id;
15826
15827 let line_ctor = LineCtor {
15829 start: Point2d {
15830 x: Expr::Number(Number {
15831 value: 0.0,
15832 units: NumericSuffix::Mm,
15833 }),
15834 y: Expr::Number(Number {
15835 value: 0.0,
15836 units: NumericSuffix::Mm,
15837 }),
15838 },
15839 end: Point2d {
15840 x: Expr::Number(Number {
15841 value: 10.0,
15842 units: NumericSuffix::Mm,
15843 }),
15844 y: Expr::Number(Number {
15845 value: 10.0,
15846 units: NumericSuffix::Mm,
15847 }),
15848 },
15849 construction: None,
15850 };
15851 let segment = SegmentCtor::Line(line_ctor);
15852 let (src_delta, scene_delta) = frontend
15853 .add_segment(&mock_ctx, version, sketch_id, segment, None)
15854 .await
15855 .unwrap();
15856 assert!(
15857 src_delta.text.contains("line(start = [0mm, 0mm], end = [10mm, 10mm])"),
15858 "Expected line in source after add, got: {}",
15859 src_delta.text
15860 );
15861 let line_id = *scene_delta.new_objects.last().unwrap();
15863
15864 let line_ctor = LineCtor {
15866 start: Point2d {
15867 x: Expr::Number(Number {
15868 value: 1.0,
15869 units: NumericSuffix::Mm,
15870 }),
15871 y: Expr::Number(Number {
15872 value: 2.0,
15873 units: NumericSuffix::Mm,
15874 }),
15875 },
15876 end: Point2d {
15877 x: Expr::Number(Number {
15878 value: 13.0,
15879 units: NumericSuffix::Mm,
15880 }),
15881 y: Expr::Number(Number {
15882 value: 14.0,
15883 units: NumericSuffix::Mm,
15884 }),
15885 },
15886 construction: None,
15887 };
15888 let segments = vec![ExistingSegmentCtor {
15889 id: line_id,
15890 ctor: SegmentCtor::Line(line_ctor),
15891 }];
15892 let (src_delta, scene_delta) = frontend
15893 .edit_segments(&mock_ctx, version, sketch_id, segments)
15894 .await
15895 .unwrap();
15896 assert!(
15897 src_delta.text.contains("line(start = [1mm, 2mm], end = [13mm, 14mm])"),
15898 "Expected edited line in source, got: {}",
15899 src_delta.text
15900 );
15901 assert_eq!(scene_delta.new_objects, vec![]);
15902
15903 ctx.close().await;
15904 mock_ctx.close().await;
15905 }
15906
15907 #[test]
15908 fn test_add_variable_declaration_uses_top_level_scope_after_sketch_block() {
15909 let code = "\
15918foo1 = 1
15919sk = sketch() {
15920 p = var 1.5
15921}
159227 + 8
15923";
15924 let mut ast = crate::parsing::top_level_parse(code).unwrap();
15925 let ast::BodyItem::ExpressionStatement(stmt) = &ast.body[2] else {
15926 panic!("expected an expression statement");
15927 };
15928 let source_ref = SourceRef::new(SourceRange::from(&stmt.expression), None);
15929 let (_, cmd_return) = mutate_ast_node_by_source_ref(
15930 &mut ast,
15931 &source_ref,
15932 AstMutateCommand::AddVariableDeclaration {
15933 prefix: "foo".to_owned(),
15934 },
15935 )
15936 .unwrap();
15937 let AstMutateCommandReturn::Name(name) = cmd_return else {
15938 panic!("expected a generated name");
15939 };
15940 assert_eq!(name, "foo2");
15941 let ast::BodyItem::VariableDeclaration(decl) = &ast.body[2] else {
15942 panic!("expected the expression statement to become a variable declaration");
15943 };
15944 assert_eq!(decl.name(), "foo2");
15945 }
15946
15947 fn function_body_at(ast: &ast::Node<ast::Program>, index: usize) -> &ast::Node<ast::Program> {
15949 let ast::BodyItem::VariableDeclaration(decl) = &ast.body[index] else {
15950 panic!("expected a variable declaration");
15951 };
15952 let ast::Expr::FunctionExpression(func) = &decl.declaration.init else {
15953 panic!("expected a function expression");
15954 };
15955 &func.body
15956 }
15957
15958 fn then_block_at(ast: &ast::Node<ast::Program>, index: usize) -> &ast::Node<ast::Program> {
15961 let ast::BodyItem::VariableDeclaration(decl) = &ast.body[index] else {
15962 panic!("expected a variable declaration");
15963 };
15964 let ast::Expr::IfExpression(if_expr) = &decl.declaration.init else {
15965 panic!("expected an if expression");
15966 };
15967 &if_expr.then_val
15968 }
15969
15970 #[test]
15971 fn test_add_variable_declaration_in_function_body_uses_function_scope() {
15972 let code = "\
15977fn build() {
15978 thing1 = 1
15979 10 + 20
15980 return thing1
15981}
15982";
15983 let mut ast = crate::parsing::top_level_parse(code).unwrap();
15984 let ast::BodyItem::ExpressionStatement(stmt) = &function_body_at(&ast, 0).body[1] else {
15985 panic!("expected an expression statement");
15986 };
15987 let source_ref = SourceRef::new(SourceRange::from(&stmt.expression), None);
15988 let (_, cmd_return) = mutate_ast_node_by_source_ref(
15989 &mut ast,
15990 &source_ref,
15991 AstMutateCommand::AddVariableDeclaration {
15992 prefix: "thing".to_owned(),
15993 },
15994 )
15995 .unwrap();
15996 let AstMutateCommandReturn::Name(name) = cmd_return else {
15997 panic!("expected a generated name");
15998 };
15999 assert_eq!(name, "thing2");
16000 let body = &function_body_at(&ast, 0).body;
16001 assert_eq!(body.len(), 3);
16002 let ast::BodyItem::VariableDeclaration(decl) = &body[1] else {
16003 panic!("expected the expression statement to become a variable declaration");
16004 };
16005 assert_eq!(decl.name(), "thing2");
16006 let ast::BodyItem::VariableDeclaration(first) = &body[0] else {
16008 panic!("expected a variable declaration");
16009 };
16010 assert_eq!(first.name(), "thing1");
16011 assert!(matches!(&body[2], ast::BodyItem::ReturnStatement(_)));
16012 }
16013
16014 #[test]
16015 fn test_delete_node_in_function_body_preserves_leading_comment() {
16016 let code = "\
16020fn build() {
16021 a = 1
16022 // keep me
16023 b = 2
16024 return a
16025}
16026";
16027 let mut ast = crate::parsing::top_level_parse(code).unwrap();
16028 let ast::BodyItem::VariableDeclaration(b_decl) = &function_body_at(&ast, 0).body[1] else {
16029 panic!("expected a variable declaration");
16030 };
16031 assert_eq!(b_decl.name(), "b");
16032 let source_ref = SourceRef::new(SourceRange::from(&b_decl.declaration.init), None);
16033 mutate_ast_node_by_source_ref(&mut ast, &source_ref, AstMutateCommand::DeleteNode).unwrap();
16034 let body = &function_body_at(&ast, 0).body;
16035 assert_eq!(body.len(), 2, "expected b to be deleted");
16036 let ast::BodyItem::VariableDeclaration(first) = &body[0] else {
16037 panic!("expected a variable declaration");
16038 };
16039 assert_eq!(first.name(), "a");
16040 let ast::BodyItem::ReturnStatement(_) = &body[1] else {
16041 panic!("expected the return statement to remain");
16042 };
16043 assert!(
16044 body[1].get_comments().iter().any(|c| c.contains("keep me")),
16045 "expected the deleted item's leading comment to migrate to the next item, got: {:?}",
16046 body[1].get_comments()
16047 );
16048 }
16049
16050 #[test]
16051 fn test_add_variable_declaration_in_function_body_ignores_parameters() {
16052 let code = "\
16060fn build(thing2) {
16061 thing1 = 1
16062 10 + 20
16063 return thing1 + thing2
16064}
16065";
16066 let mut ast = crate::parsing::top_level_parse(code).unwrap();
16067 let ast::BodyItem::ExpressionStatement(stmt) = &function_body_at(&ast, 0).body[1] else {
16068 panic!("expected an expression statement");
16069 };
16070 let source_ref = SourceRef::new(SourceRange::from(&stmt.expression), None);
16071 let (_, cmd_return) = mutate_ast_node_by_source_ref(
16072 &mut ast,
16073 &source_ref,
16074 AstMutateCommand::AddVariableDeclaration {
16075 prefix: "thing".to_owned(),
16076 },
16077 )
16078 .unwrap();
16079 let AstMutateCommandReturn::Name(name) = cmd_return else {
16080 panic!("expected a generated name");
16081 };
16082 assert_eq!(name, "thing2", "locals are avoided, but parameters are not");
16083 }
16084
16085 #[test]
16086 fn test_add_variable_declaration_in_if_branch_uses_branch_scope() {
16087 let code = "\
16088x = 1
16089y = if x > 0 {
16090 q1 = 1
16091 foo(q1)
16092 q1
16093} else {
16094 2
16095}
16096";
16097 let mut ast = crate::parsing::top_level_parse(code).unwrap();
16098 let ast::BodyItem::ExpressionStatement(stmt) = &then_block_at(&ast, 1).body[1] else {
16099 panic!("expected an expression statement");
16100 };
16101 let source_ref = SourceRef::new(SourceRange::from(&stmt.expression), None);
16102 let (_, cmd_return) = mutate_ast_node_by_source_ref(
16103 &mut ast,
16104 &source_ref,
16105 AstMutateCommand::AddVariableDeclaration { prefix: "q".to_owned() },
16106 )
16107 .unwrap();
16108 let AstMutateCommandReturn::Name(name) = cmd_return else {
16109 panic!("expected a generated name");
16110 };
16111 assert_eq!(name, "q2");
16112 let body = &then_block_at(&ast, 1).body;
16113 assert_eq!(body.len(), 3);
16114 let ast::BodyItem::VariableDeclaration(decl) = &body[1] else {
16115 panic!("expected the expression statement to become a variable declaration");
16116 };
16117 assert_eq!(decl.name(), "q2");
16118 let ast::BodyItem::VariableDeclaration(first) = &body[0] else {
16120 panic!("expected a variable declaration");
16121 };
16122 assert_eq!(first.name(), "q1");
16123 assert!(matches!(&body[2], ast::BodyItem::ExpressionStatement(_)));
16124 }
16125
16126 #[test]
16127 fn test_delete_node_in_if_branch_preserves_leading_comment() {
16128 let code = "\
16131y = if true {
16132 a = 1
16133 // keep me
16134 b = 2
16135 a + b
16136} else {
16137 2
16138}
16139";
16140 let mut ast = crate::parsing::top_level_parse(code).unwrap();
16141 let ast::BodyItem::VariableDeclaration(b_decl) = &then_block_at(&ast, 0).body[1] else {
16142 panic!("expected a variable declaration");
16143 };
16144 assert_eq!(b_decl.name(), "b");
16145 let source_ref = SourceRef::new(SourceRange::from(&b_decl.declaration.init), None);
16146 mutate_ast_node_by_source_ref(&mut ast, &source_ref, AstMutateCommand::DeleteNode).unwrap();
16147 let body = &then_block_at(&ast, 0).body;
16148 assert_eq!(body.len(), 2, "expected b to be deleted");
16149 let ast::BodyItem::VariableDeclaration(first) = &body[0] else {
16150 panic!("expected a variable declaration");
16151 };
16152 assert_eq!(first.name(), "a");
16153 let ast::BodyItem::ExpressionStatement(_) = &body[1] else {
16154 panic!("expected the tail expression to remain");
16155 };
16156 assert!(
16157 body[1].get_comments().iter().any(|c| c.contains("keep me")),
16158 "expected the deleted item's leading comment to migrate to the next item, got: {:?}",
16159 body[1].get_comments()
16160 );
16161 }
16162}