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::cache::clear_mem_cache;
71use crate::execution::cache::read_old_memory;
72use crate::execution::cache::write_old_memory;
73use crate::execution::types::adjust_length;
74use crate::fmt::format_number_literal;
75use crate::front::CoincidentExt;
76use crate::front::DistanceExt;
77use crate::front::Error;
78use crate::front::ExecResult;
79use crate::front::ObjectKindExt;
80use crate::front::SegmentExt;
81use crate::frontend::api::FileId;
82use crate::frontend::api::ProjectId;
83use crate::frontend::api::RestoreSketchCheckpointOutcome;
84use crate::frontend::api::SceneGraph;
85use crate::frontend::api::SceneGraphDelta;
86use crate::frontend::api::SketchCheckpointId;
87use crate::frontend::api::SourceDelta;
88use crate::frontend::api::Version;
89use crate::frontend::modify::find_defined_names;
90use crate::frontend::modify::next_free_name;
91use crate::frontend::modify::next_free_name_with_padding;
92use crate::frontend::sketch::ConstraintLabelPositionEdit;
93use crate::frontend::sketch::ExistingSegmentCtor;
94use crate::frontend::sketch::SketchApi;
95use crate::id::IncIdGenerator;
96use crate::parsing::ast::types as ast;
97use crate::parsing::ast::types::BoxNode;
98use crate::parsing::ast::types::CallExpressionKw;
99use crate::parsing::ast::types::NodePathExt;
100use crate::std::constraints::LinesAtAngleKind;
101use crate::walk::NodeMut;
102use crate::walk::Visitable;
103use crate::walk::traverse::MutateBodyItem;
104use crate::walk::traverse::TraversalReturn;
105use crate::walk::traverse::Visitor;
106use crate::walk::traverse::dfs_mut;
107
108pub(crate) mod api;
109pub(crate) mod modify;
110pub(crate) mod sketch;
111
112pub const MAX_SKETCH_CHECKPOINTS: usize = 100;
113
114#[derive(Debug, Clone)]
115struct SketchCheckpoint {
116 id: SketchCheckpointId,
117 source: SourceDelta,
118 program: Program,
119 scene_graph: SceneGraph,
120 exec_outcome: ExecOutcome,
121 point_freedom_cache: HashMap<ObjectId, Freedom>,
122 mock_memory: Option<SketchModeState>,
123}
124pub(crate) mod trim;
125
126struct ArcSizeConstraintParams {
127 points: Vec<ObjectId>,
128 function_name: &'static str,
129 value: f64,
130 units: NumericSuffix,
131 label_position: Option<Point2d<Number>>,
132 constraint_type_name: &'static str,
133}
134
135const POINT_FN: &str = "point";
136const POINT_AT_PARAM: &str = "at";
137const LINE_FN: &str = "line";
138const LINE_VARIABLE: &str = "line";
139const LINE_START_PARAM: &str = "start";
140const LINE_END_PARAM: &str = "end";
141const ARC_FN: &str = "arc";
142const ARC_VARIABLE: &str = "arc";
143const ARC_START_PARAM: &str = "start";
144const ARC_END_PARAM: &str = "end";
145const ARC_CENTER_PARAM: &str = "center";
146const ARC_DIRECTION_PARAM: &str = "direction";
147const ARC_DIRECTION_CW_NAME: &str = "CW";
149const CIRCLE_FN: &str = "circle";
150const CIRCLE_VARIABLE: &str = "circle";
151const CIRCLE_START_PARAM: &str = "start";
152const CIRCLE_CENTER_PARAM: &str = "center";
153const CONTROL_POINT_SPLINE_FN: &str = "controlPointSpline";
154const CONTROL_POINT_SPLINE_POINTS_PARAM: &str = "points";
155const LABEL_POSITION_PARAM: &str = "labelPosition";
156
157const COINCIDENT_FN: &str = "coincident";
158const DIAMETER_FN: &str = "diameter";
159const DISTANCE_FN: &str = "distance";
160const FIXED_FN: &str = "fixed";
161const ANGLE_FN: &str = "angle";
162const ANGLE_DIMENSION_FN: &str = "angleDimension";
163const ANGLE_LINES_PARAM: &str = "lines";
164const ANGLE_SECTOR_PARAM: &str = "sector";
165const ANGLE_INVERSE_PARAM: &str = "inverse";
166const HORIZONTAL_DISTANCE_FN: &str = "horizontalDistance";
167const VERTICAL_DISTANCE_FN: &str = "verticalDistance";
168const EQUAL_LENGTH_FN: &str = "equalLength";
169const EQUAL_RADIUS_FN: &str = "equalRadius";
170const HORIZONTAL_FN: &str = "horizontal";
171const MIDPOINT_FN: &str = "midpoint";
172const MIDPOINT_POINT_PARAM: &str = "point";
173const RADIUS_FN: &str = "radius";
174const SYMMETRIC_FN: &str = "symmetric";
175const SYMMETRIC_AXIS_PARAM: &str = "axis";
176const TANGENT_FN: &str = "tangent";
177const VERTICAL_FN: &str = "vertical";
178
179const LINE_PROPERTY_START: &str = "start";
180const LINE_PROPERTY_END: &str = "end";
181
182const ARC_PROPERTY_START: &str = "start";
183const ARC_PROPERTY_END: &str = "end";
184const ARC_PROPERTY_CENTER: &str = "center";
185const CIRCLE_PROPERTY_START: &str = "start";
186const CIRCLE_PROPERTY_CENTER: &str = "center";
187const CONTROL_POINT_SPLINE_PROPERTY_CONTROLS: &str = "controls";
188const CONTROL_POINT_SPLINE_PROPERTY_EDGES: &str = "edges";
189
190const CONSTRUCTION_PARAM: &str = "construction";
191
192#[derive(Debug, Clone, Copy)]
193enum EditDeleteKind {
194 Edit,
195 DeleteNonSketch,
196}
197
198struct ExecuteAfterEditOptions {
200 segment_ids_edited: AhashIndexSet<ObjectId>,
201 edit_kind: EditDeleteKind,
202 commit_solved_initial_guesses: bool,
203}
204
205impl EditDeleteKind {
206 fn is_delete(&self) -> bool {
208 match self {
209 EditDeleteKind::Edit => false,
210 EditDeleteKind::DeleteNonSketch => true,
211 }
212 }
213
214 fn to_change_kind(self) -> ChangeKind {
215 match self {
216 EditDeleteKind::Edit => ChangeKind::Edit,
217 EditDeleteKind::DeleteNonSketch => ChangeKind::Delete,
218 }
219 }
220}
221
222#[derive(Debug, Clone, Copy)]
223enum ChangeKind {
224 Add,
225 Edit,
226 Delete,
227 None,
228}
229
230#[derive(Debug, Clone, Serialize, ts_rs::TS)]
231#[ts(export, export_to = "FrontendApi.ts")]
232#[serde(tag = "type")]
233pub enum SetProgramOutcome {
234 #[serde(rename_all = "camelCase")]
235 Success {
236 scene_graph: Box<SceneGraph>,
237 exec_outcome: Box<ExecOutcome>,
238 checkpoint_id: Option<SketchCheckpointId>,
239 },
240 #[serde(rename_all = "camelCase")]
241 ExecFailure { error: Box<KclErrorWithOutputs> },
242}
243
244pub struct EditSegmentsOptions {
246 pub anchor_segment_ids: Option<Vec<ObjectId>>,
252 pub drag_anchors: Vec<SegmentDragAnchor>,
255 pub constraint_label_edits: Vec<ConstraintLabelPositionEdit>,
258 pub commit_solved_initial_guesses: bool,
260}
261
262pub struct EditDistanceConstraintLabelPositionOptions {
264 pub anchor_segment_ids: Vec<ObjectId>,
266 pub commit_solved_initial_guesses: bool,
268}
269
270pub struct EditConstraintOptions {
272 pub commit_solved_initial_guesses: bool,
274}
275
276#[derive(Debug, Clone)]
277struct SolidAstReference {
278 variable_name: String,
279 output_index: Option<usize>,
280}
281
282#[derive(Debug, Clone)]
283pub struct FrontendState {
284 program: Program,
285 scene_graph: SceneGraph,
286 solid_references: HashMap<Uuid, SolidAstReference>,
288 point_freedom_cache: HashMap<ObjectId, Freedom>,
291 next_drag_anchor_segment_ids: Option<AhashIndexSet<ObjectId>>,
294 next_segment_drag_anchors: Option<Vec<SegmentDragAnchor>>,
297 next_constraint_label_edits: Option<Vec<ConstraintLabelPositionEdit>>,
299 next_edit_commits_solver_solutions: Option<bool>,
303 sketch_checkpoints: VecDeque<SketchCheckpoint>,
304 sketch_checkpoint_id_gen: IncIdGenerator<u64>,
305}
306
307impl Default for FrontendState {
308 fn default() -> Self {
309 Self::new()
310 }
311}
312
313impl FrontendState {
314 pub fn new() -> Self {
315 Self {
316 program: Program::empty(),
317 scene_graph: SceneGraph {
318 project: ProjectId(0),
319 file: FileId(0),
320 version: Version(0),
321 objects: Default::default(),
322 settings: Default::default(),
323 sketch_mode: Default::default(),
324 },
325 solid_references: HashMap::new(),
326 point_freedom_cache: HashMap::new(),
327 next_drag_anchor_segment_ids: None,
328 next_segment_drag_anchors: None,
329 next_constraint_label_edits: None,
330 next_edit_commits_solver_solutions: None,
331 sketch_checkpoints: VecDeque::new(),
332 sketch_checkpoint_id_gen: IncIdGenerator::new(1),
333 }
334 }
335
336 pub fn scene_graph(&self) -> &SceneGraph {
338 &self.scene_graph
339 }
340
341 pub fn default_length_unit(&self) -> UnitLength {
342 self.program
343 .meta_settings()
344 .ok()
345 .flatten()
346 .map(|settings| settings.default_length_units)
347 .unwrap_or(UnitLength::Millimeters)
348 }
349
350 pub async fn create_sketch_checkpoint(&mut self, exec_outcome: ExecOutcome) -> api::Result<SketchCheckpointId> {
351 let checkpoint_id = SketchCheckpointId::new(self.sketch_checkpoint_id_gen.next_id());
352
353 let checkpoint = SketchCheckpoint {
354 id: checkpoint_id,
355 source: SourceDelta {
356 text: source_from_ast(&self.program.ast),
357 },
358 program: self.program.clone(),
359 scene_graph: self.scene_graph.clone(),
360 exec_outcome,
361 point_freedom_cache: self.point_freedom_cache.clone(),
362 mock_memory: read_old_memory().await,
363 };
364
365 self.sketch_checkpoints.push_back(checkpoint);
366 while self.sketch_checkpoints.len() > MAX_SKETCH_CHECKPOINTS {
367 self.sketch_checkpoints.pop_front();
368 }
369
370 Ok(checkpoint_id)
371 }
372
373 pub async fn edit_segments_with_options(
381 &mut self,
382 ctx: &ExecutorContext,
383 version: Version,
384 sketch: ObjectId,
385 segments: Vec<ExistingSegmentCtor>,
386 options: EditSegmentsOptions,
387 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
388 let previous_anchor_ids = options.anchor_segment_ids.map(|anchor_ids| {
389 self.next_drag_anchor_segment_ids
390 .replace(anchor_ids.into_iter().collect())
391 });
392 let previous_drag_anchors = self.next_segment_drag_anchors.replace(options.drag_anchors);
393 let previous_constraint_label_edits = self.next_constraint_label_edits.replace(options.constraint_label_edits);
394 let previous_commit_mode = self
395 .next_edit_commits_solver_solutions
396 .replace(options.commit_solved_initial_guesses);
397 let result = SketchApi::edit_segments(self, ctx, version, sketch, segments).await;
398 if let Some(previous_anchor_ids) = previous_anchor_ids {
399 self.next_drag_anchor_segment_ids = previous_anchor_ids;
400 }
401 self.next_segment_drag_anchors = previous_drag_anchors;
402 self.next_constraint_label_edits = previous_constraint_label_edits;
403 self.next_edit_commits_solver_solutions = previous_commit_mode;
404 result
405 }
406
407 pub async fn edit_distance_constraint_label_position_with_options(
413 &mut self,
414 ctx: &ExecutorContext,
415 version: Version,
416 sketch: ObjectId,
417 constraint_id: ObjectId,
418 label_position: Point2d<Number>,
419 options: EditDistanceConstraintLabelPositionOptions,
420 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
421 let previous_commit_mode = self
422 .next_edit_commits_solver_solutions
423 .replace(options.commit_solved_initial_guesses);
424 let result = SketchApi::edit_distance_constraint_label_position(
425 self,
426 ctx,
427 version,
428 sketch,
429 constraint_id,
430 label_position,
431 options.anchor_segment_ids,
432 )
433 .await;
434 self.next_edit_commits_solver_solutions = previous_commit_mode;
435 result
436 }
437
438 pub async fn edit_distance_constraint_with_options(
440 &mut self,
441 ctx: &ExecutorContext,
442 version: Version,
443 sketch: ObjectId,
444 constraint_id: ObjectId,
445 constraint: Constraint,
446 options: EditConstraintOptions,
447 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
448 self.edit_constraint_with_options(ctx, version, sketch, constraint_id, constraint, options)
449 .await
450 }
451
452 pub async fn edit_angle_constraint_with_options(
454 &mut self,
455 ctx: &ExecutorContext,
456 version: Version,
457 sketch: ObjectId,
458 constraint_id: ObjectId,
459 angle: Angle,
460 options: EditConstraintOptions,
461 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
462 self.edit_constraint_with_options(ctx, version, sketch, constraint_id, Constraint::Angle(angle), options)
463 .await
464 }
465
466 pub async fn edit_constraint_with_options(
468 &mut self,
469 ctx: &ExecutorContext,
470 _version: Version,
471 sketch: ObjectId,
472 constraint_id: ObjectId,
473 constraint: Constraint,
474 options: EditConstraintOptions,
475 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
476 let sketch_block_ref =
478 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
479
480 let object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
481 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Object not found: {constraint_id:?}")))
482 })?;
483
484 let mut new_ast = self.program.ast.clone();
485 let command = match &object.kind {
486 ObjectKind::Constraint {
487 constraint:
488 Constraint::Distance(_) | Constraint::HorizontalDistance(_) | Constraint::VerticalDistance(_),
489 } => {
490 let (function_name, distance) = match &constraint {
491 Constraint::Distance(distance) => (DISTANCE_FN, distance),
492 Constraint::HorizontalDistance(distance) => (HORIZONTAL_DISTANCE_FN, distance),
493 Constraint::VerticalDistance(distance) => (VERTICAL_DISTANCE_FN, distance),
494 _ => {
495 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
496 "A distance constraint can only be replaced by another distance constraint".to_owned(),
497 )));
498 }
499 };
500 let (call, value) = self
501 .distance_constraint_ast_parts(function_name, distance, &mut new_ast)
502 .map_err(KclErrorWithOutputs::no_outputs)?;
503 AstMutateCommand::EditDistanceConstraint { call, value }
504 }
505 ObjectKind::Constraint {
506 constraint: Constraint::Angle(_),
507 } => {
508 let Constraint::Angle(angle) = &constraint else {
509 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
510 "An angle constraint can only be replaced by another angle constraint".to_owned(),
511 )));
512 };
513 let (call, value) = self
514 .angle_constraint_ast_parts(angle, &mut new_ast)
515 .map_err(KclErrorWithOutputs::no_outputs)?;
516 AstMutateCommand::EditAngleConstraint { call, value }
517 }
518 ObjectKind::Constraint { .. } => {
519 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
520 "Editing {} is not supported",
521 object.kind.human_friendly_kind_with_article(),
522 ))));
523 }
524 _ => {
525 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
526 "Object is not a constraint: {constraint_id:?}"
527 ))));
528 }
529 };
530
531 self.mutate_ast(&mut new_ast, constraint_id, command)
532 .map_err(KclErrorWithOutputs::no_outputs)?;
533
534 self.execute_after_edit(
535 ctx,
536 sketch,
537 sketch_block_ref,
538 &mut new_ast,
539 ExecuteAfterEditOptions {
540 segment_ids_edited: Default::default(),
541 edit_kind: EditDeleteKind::Edit,
542 commit_solved_initial_guesses: options.commit_solved_initial_guesses,
543 },
544 )
545 .await
546 }
547
548 pub async fn restore_sketch_checkpoint(
549 &mut self,
550 checkpoint_id: SketchCheckpointId,
551 ) -> api::Result<RestoreSketchCheckpointOutcome> {
552 let checkpoint = self
553 .sketch_checkpoints
554 .iter()
555 .find(|checkpoint| checkpoint.id == checkpoint_id)
556 .cloned()
557 .ok_or_else(|| Error {
558 msg: format!("Sketch checkpoint not found: {checkpoint_id:?}"),
559 })?;
560
561 self.program = checkpoint.program;
562 self.scene_graph = checkpoint.scene_graph.clone();
563 self.solid_references = solid_references_from_variables(&self.program.ast, &checkpoint.exec_outcome.variables);
564 self.point_freedom_cache = checkpoint.point_freedom_cache;
565 self.next_drag_anchor_segment_ids = None;
566 self.next_segment_drag_anchors = None;
567 self.next_constraint_label_edits = None;
568 self.next_edit_commits_solver_solutions = None;
569
570 if let Some(mock_memory) = checkpoint.mock_memory {
571 write_old_memory(mock_memory).await;
572 } else {
573 clear_mem_cache().await;
574 }
575
576 Ok(RestoreSketchCheckpointOutcome {
577 source_delta: checkpoint.source,
578 scene_graph_delta: SceneGraphDelta {
579 new_graph: self.scene_graph_for_ui(),
580 new_objects: Vec::new(),
581 invalidates_ids: true,
582 exec_outcome: checkpoint.exec_outcome,
583 },
584 })
585 }
586
587 pub fn clear_sketch_checkpoints(&mut self) {
588 self.sketch_checkpoints.clear();
589 }
590 fn scene_graph_for_ui(&self) -> SceneGraph {
591 let has_control_point_splines = self.scene_graph.objects.iter().any(|object| {
592 matches!(
593 object.kind,
594 ObjectKind::Segment {
595 segment: Segment::ControlPointSpline(_)
596 }
597 )
598 });
599
600 if !has_control_point_splines {
601 return self.scene_graph.clone();
602 }
603
604 let hidden_constraint_ids = self
605 .scene_graph
606 .objects
607 .iter()
608 .filter_map(|object| match &object.kind {
609 ObjectKind::Constraint {
610 constraint: Constraint::Coincident(coincident),
611 } if coincident_is_internal_to_same_control_point_spline(coincident, &self.scene_graph) => {
612 Some(object.id)
613 }
614 _ => None,
615 })
616 .collect::<HashSet<_>>();
617
618 if hidden_constraint_ids.is_empty() {
619 return self.scene_graph.clone();
620 }
621
622 let mut scene_graph = self.scene_graph.clone();
623 for object in &mut scene_graph.objects {
624 match &mut object.kind {
625 ObjectKind::Constraint { .. } if hidden_constraint_ids.contains(&object.id) => {
626 object.kind = ObjectKind::Nil;
627 }
628 ObjectKind::Sketch(sketch) => {
629 sketch
630 .constraints
631 .retain(|constraint_id| !hidden_constraint_ids.contains(constraint_id));
632 }
633 _ => {}
634 }
635 }
636
637 scene_graph
638 }
639}
640
641fn coincident_is_internal_to_same_control_point_spline(coincident: &Coincident, scene_graph: &SceneGraph) -> bool {
642 let mut first_owner_id = None;
643 for segment_id in coincident.segment_ids() {
644 let Some(owner_id) = owning_control_point_spline_id(segment_id, scene_graph) else {
645 return false;
646 };
647
648 match first_owner_id {
649 Some(first_owner_id) if first_owner_id != owner_id => return false,
650 Some(_) => {}
651 None => first_owner_id = Some(owner_id),
652 }
653 }
654
655 first_owner_id.is_some()
656}
657
658fn owning_control_point_spline_id(segment_id: ObjectId, scene_graph: &SceneGraph) -> Option<ObjectId> {
659 let object = scene_graph.objects.get(segment_id.0)?;
660 let ObjectKind::Segment { segment } = &object.kind else {
661 return None;
662 };
663
664 match segment {
665 Segment::ControlPointSpline(_) => Some(segment_id),
666 Segment::Point(point) => point
667 .owner
668 .filter(|owner_id| matches_control_point_spline_owner(*owner_id, scene_graph)),
669 Segment::Line(line) => line
670 .owner
671 .filter(|owner_id| matches_control_point_spline_owner(*owner_id, scene_graph)),
672 _ => None,
673 }
674}
675
676fn matches_control_point_spline_owner(owner_id: ObjectId, scene_graph: &SceneGraph) -> bool {
677 matches!(
678 scene_graph.objects.get(owner_id.0).map(|object| &object.kind),
679 Some(ObjectKind::Segment {
680 segment: Segment::ControlPointSpline(_)
681 })
682 )
683}
684
685fn ensure_control_point_spline_experimental_features(program: &Program) -> Result<Program, KclError> {
686 let experimental_features_allowed = program
687 .meta_settings()
688 .ok()
689 .flatten()
690 .map(|settings| settings.experimental_features == WarningLevel::Allow)
691 .unwrap_or(false);
692 if experimental_features_allowed {
693 return Ok(program.clone());
694 }
695
696 program.change_experimental_features(Some(WarningLevel::Allow))
697}
698
699impl SketchApi for FrontendState {
700 async fn execute_mock(
701 &mut self,
702 ctx: &ExecutorContext,
703 _version: Version,
704 sketch: ObjectId,
705 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
706 let sketch_block_ref =
707 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
708
709 let mut truncated_program = self.program.clone();
710 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::None)
711 .map_err(KclErrorWithOutputs::no_outputs)?;
712
713 let outcome = ctx
715 .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch))
716 .await?;
717 let new_source = source_from_ast(&self.program.ast);
718 let src_delta = SourceDelta { text: new_source };
719 let outcome = self.update_state_after_exec(outcome, true);
721 let scene_graph_delta = SceneGraphDelta {
722 new_graph: self.scene_graph.clone(),
723 new_objects: Default::default(),
724 invalidates_ids: false,
725 exec_outcome: outcome,
726 };
727 Ok((src_delta, scene_graph_delta))
728 }
729
730 async fn new_sketch(
731 &mut self,
732 ctx: &ExecutorContext,
733 _project: ProjectId,
734 _file: FileId,
735 _version: Version,
736 args: SketchCtor,
737 ) -> ExecResult<(SourceDelta, SceneGraphDelta, ObjectId)> {
738 let mut new_ast = self.program.ast.clone();
741 let mut plane_ast = sketch_on_ast_expr(&mut new_ast, &self.scene_graph, &self.solid_references, &args.on)
743 .map_err(KclErrorWithOutputs::no_outputs)?;
744 let mut defined_names = find_defined_names(&new_ast);
745 let is_face_of_expr = matches!(
746 &plane_ast,
747 ast::Expr::CallExpressionKw(call) if call.callee.name.name == "faceOf"
748 );
749 if is_face_of_expr {
750 let face_name = next_free_name_with_padding("face", &defined_names)
751 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
752 let face_decl = ast::VariableDeclaration::new(
753 ast::VariableDeclarator::new(&face_name, plane_ast),
754 ast::ItemVisibility::Default,
755 ast::VariableKind::Const,
756 );
757 new_ast
758 .body
759 .push(ast::BodyItem::VariableDeclaration(BoxNode::new(ast::Node::no_src(
760 face_decl,
761 ))));
762 defined_names.insert(face_name.clone());
763 plane_ast = ast::Expr::Name(BoxNode::new(ast::Name::new(&face_name)));
764 }
765 let sketch_ast = ast::SketchBlock {
766 arguments: vec![ast::LabeledArg {
767 label: Some(ast::Identifier::new(SKETCH_BLOCK_PARAM_ON)),
768 arg: plane_ast,
769 }],
770 body: Default::default(),
771 is_being_edited: false,
772 non_code_meta: Default::default(),
773 digest: None,
774 };
775 let sketch_name = next_free_name_with_padding("sketch", &defined_names)
778 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
779 let sketch_decl = ast::VariableDeclaration::new(
780 ast::VariableDeclarator::new(
781 &sketch_name,
782 ast::Expr::SketchBlock(BoxNode::new(ast::Node::no_src(sketch_ast))),
783 ),
784 ast::ItemVisibility::Default,
785 ast::VariableKind::Const,
786 );
787 new_ast
788 .body
789 .push(ast::BodyItem::VariableDeclaration(BoxNode::new(ast::Node::no_src(
790 sketch_decl,
791 ))));
792 let new_source = source_from_ast(&new_ast);
794 let new_program = parse_frontend_mutation_source(
796 &new_source,
797 "Error parsing KCL source after adding sketch",
798 "No AST produced after adding sketch",
799 )?;
800
801 self.program = new_program.clone();
803
804 let outcome = ctx.run_with_caching(new_program.clone()).await?;
807 let freedom_analysis_ran = true;
808
809 let outcome = self.update_state_after_exec(outcome, freedom_analysis_ran);
810
811 let Some(sketch_id) = self
812 .scene_graph
813 .objects
814 .iter()
815 .filter_map(|object| match object.kind {
816 ObjectKind::Sketch(_) => Some(object.id),
817 _ => None,
818 })
819 .max_by_key(|id| id.0)
820 else {
821 return Err(KclErrorWithOutputs::from_error_outcome(
822 KclError::refactor("No objects in scene graph after adding sketch".to_owned()),
823 outcome,
824 ));
825 };
826 self.scene_graph.sketch_mode = Some(sketch_id);
828
829 let src_delta = SourceDelta { text: new_source };
830 let scene_graph_delta = SceneGraphDelta {
831 new_graph: self.scene_graph_for_ui(),
832 invalidates_ids: false,
833 new_objects: vec![sketch_id],
834 exec_outcome: outcome,
835 };
836 Ok((src_delta, scene_graph_delta, sketch_id))
837 }
838
839 async fn edit_sketch(
840 &mut self,
841 ctx: &ExecutorContext,
842 _project: ProjectId,
843 _file: FileId,
844 _version: Version,
845 sketch: ObjectId,
846 ) -> ExecResult<SceneGraphDelta> {
847 let sketch_object = self.scene_graph.objects.get(sketch.0).ok_or_else(|| {
851 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
852 })?;
853 let ObjectKind::Sketch(_) = &sketch_object.kind else {
854 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
855 "Object is not a sketch, it is {}",
856 sketch_object.kind.human_friendly_kind_with_article()
857 ))));
858 };
859 let sketch_block_ref = expect_single_node_ref(sketch_object).map_err(KclErrorWithOutputs::no_outputs)?;
860
861 self.scene_graph.sketch_mode = Some(sketch);
863
864 let mut truncated_program = self.program.clone();
866 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::None)
867 .map_err(KclErrorWithOutputs::no_outputs)?;
868
869 let outcome = ctx
872 .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch))
873 .await?;
874
875 let outcome = self.update_state_after_exec(outcome, true);
877 let scene_graph_delta = SceneGraphDelta {
878 new_graph: self.scene_graph_for_ui(),
879 invalidates_ids: false,
880 new_objects: Vec::new(),
881 exec_outcome: outcome,
882 };
883 Ok(scene_graph_delta)
884 }
885
886 async fn exit_sketch(
887 &mut self,
888 ctx: &ExecutorContext,
889 _version: Version,
890 sketch: ObjectId,
891 ) -> ExecResult<SceneGraph> {
892 #[cfg(not(target_arch = "wasm32"))]
894 let _ = sketch;
895 #[cfg(target_arch = "wasm32")]
896 if self.scene_graph.sketch_mode != Some(sketch) {
897 web_sys::console::warn_1(
898 &format!(
899 "WARNING: exit_sketch: current state's sketch mode ID doesn't match the given sketch ID; state={:#?}, given={sketch:?}",
900 self.scene_graph.sketch_mode
901 )
902 .into(),
903 );
904 }
905 self.scene_graph.sketch_mode = None;
906
907 let outcome = ctx.run_with_caching(self.program.clone()).await?;
909
910 self.update_state_after_exec(outcome, false);
912
913 Ok(self.scene_graph_for_ui())
914 }
915
916 async fn delete_sketch(
917 &mut self,
918 ctx: &ExecutorContext,
919 _version: Version,
920 sketch: ObjectId,
921 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
922 let mut new_ast = self.program.ast.clone();
925
926 let sketch_id = sketch;
928 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
929 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
930 })?;
931 let ObjectKind::Sketch(_) = &sketch_object.kind else {
932 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
933 "Object is not a sketch, it is {}",
934 sketch_object.kind.human_friendly_kind_with_article(),
935 ))));
936 };
937
938 self.mutate_ast(&mut new_ast, sketch_id, AstMutateCommand::DeleteNode)
940 .map_err(KclErrorWithOutputs::no_outputs)?;
941
942 self.execute_after_delete_sketch(ctx, &mut new_ast).await
943 }
944
945 async fn add_segment(
946 &mut self,
947 ctx: &ExecutorContext,
948 _version: Version,
949 sketch: ObjectId,
950 segment: SegmentCtor,
951 _label: Option<String>,
952 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
953 match segment {
955 SegmentCtor::Point(ctor) => self.add_point(ctx, sketch, ctor).await,
956 SegmentCtor::Line(ctor) => self.add_line(ctx, sketch, ctor).await,
957 SegmentCtor::Arc(ctor) => self.add_arc(ctx, sketch, ctor).await,
958 SegmentCtor::Circle(ctor) => self.add_circle(ctx, sketch, ctor).await,
959 SegmentCtor::ControlPointSpline(ctor) => self.add_control_point_spline(ctx, sketch, ctor).await,
960 }
961 }
962
963 async fn edit_segments(
964 &mut self,
965 ctx: &ExecutorContext,
966 _version: Version,
967 sketch: ObjectId,
968 segments: Vec<ExistingSegmentCtor>,
969 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
970 let sketch_block_ref =
972 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
973
974 let mut new_ast = self.program.ast.clone();
975 let mut edited_segment_ids = AhashIndexSet::with_capacity_and_hasher(segments.len(), Default::default());
976 let mut invalidates_ids = false;
977
978 for segment in &segments {
981 edited_segment_ids.insert(segment.id);
982 if let SegmentCtor::ControlPointSpline(new_ctor) = &segment.ctor
983 && let Some(existing_object) = self.scene_graph.objects.get(segment.id.0)
984 && let ObjectKind::Segment {
985 segment: Segment::ControlPointSpline(existing_spline),
986 } = &existing_object.kind
987 && existing_spline.controls.len() != new_ctor.points.len()
988 {
989 invalidates_ids = true;
990 }
991 }
992 let drag_anchor_segment_ids = self
993 .next_drag_anchor_segment_ids
994 .take()
995 .unwrap_or_else(|| edited_segment_ids.clone());
996 let constraint_label_edits = self.next_constraint_label_edits.take().unwrap_or_default();
997 let commit_solved_initial_guesses = self.next_edit_commits_solver_solutions.take().unwrap_or(true);
998
999 let mut final_edits: IndexMap<ObjectId, SegmentCtor> = IndexMap::new();
1014
1015 for segment in segments {
1016 let segment_id = segment.id;
1017 match segment.ctor {
1018 SegmentCtor::Point(ctor) => {
1019 if let Some(segment_object) = self.scene_graph.objects.get(segment_id.0)
1021 && let ObjectKind::Segment { segment } = &segment_object.kind
1022 && let Segment::Point(point) = segment
1023 && let Some(owner_id) = point.owner
1024 && let Some(owner_object) = self.scene_graph.objects.get(owner_id.0)
1025 && let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind
1026 {
1027 match owner_segment {
1028 Segment::Line(line) if line.start == segment_id || line.end == segment_id => {
1029 if let Some(existing) = final_edits.get_mut(&owner_id) {
1030 let SegmentCtor::Line(line_ctor) = existing else {
1031 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1032 "Internal: Expected line ctor for owner, but found {}",
1033 existing.human_friendly_kind_with_article()
1034 ))));
1035 };
1036 if line.start == segment_id {
1038 line_ctor.start = ctor.position;
1039 } else {
1040 line_ctor.end = ctor.position;
1041 }
1042 } else if let SegmentCtor::Line(line_ctor) = &line.ctor {
1043 let mut line_ctor = line_ctor.clone();
1045 if line.start == segment_id {
1046 line_ctor.start = ctor.position;
1047 } else {
1048 line_ctor.end = ctor.position;
1049 }
1050 final_edits.insert(owner_id, SegmentCtor::Line(line_ctor));
1051 } else {
1052 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1054 "Internal: Line does not have line ctor, but found {}",
1055 line.ctor.human_friendly_kind_with_article()
1056 ))));
1057 }
1058 continue;
1059 }
1060 Segment::Arc(arc)
1061 if arc.start == segment_id || arc.end == segment_id || arc.center == segment_id =>
1062 {
1063 if let Some(existing) = final_edits.get_mut(&owner_id) {
1064 let SegmentCtor::Arc(arc_ctor) = existing else {
1065 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1066 "Internal: Expected arc ctor for owner, but found {}",
1067 existing.human_friendly_kind_with_article()
1068 ))));
1069 };
1070 if arc.start == segment_id {
1071 arc_ctor.start = ctor.position;
1072 } else if arc.end == segment_id {
1073 arc_ctor.end = ctor.position;
1074 } else {
1075 arc_ctor.center = ctor.position;
1076 }
1077 } else if let SegmentCtor::Arc(arc_ctor) = &arc.ctor {
1078 let mut arc_ctor = arc_ctor.clone();
1079 if arc.start == segment_id {
1080 arc_ctor.start = ctor.position;
1081 } else if arc.end == segment_id {
1082 arc_ctor.end = ctor.position;
1083 } else {
1084 arc_ctor.center = ctor.position;
1085 }
1086 final_edits.insert(owner_id, SegmentCtor::Arc(arc_ctor));
1087 } else {
1088 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1089 "Internal: Arc does not have arc ctor, but found {}",
1090 arc.ctor.human_friendly_kind_with_article()
1091 ))));
1092 }
1093 continue;
1094 }
1095 Segment::Circle(circle) if circle.start == segment_id || circle.center == segment_id => {
1096 if let Some(existing) = final_edits.get_mut(&owner_id) {
1097 let SegmentCtor::Circle(circle_ctor) = existing else {
1098 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1099 "Internal: Expected circle ctor for owner, but found {}",
1100 existing.human_friendly_kind_with_article()
1101 ))));
1102 };
1103 if circle.start == segment_id {
1104 circle_ctor.start = ctor.position;
1105 } else {
1106 circle_ctor.center = ctor.position;
1107 }
1108 } else if let SegmentCtor::Circle(circle_ctor) = &circle.ctor {
1109 let mut circle_ctor = circle_ctor.clone();
1110 if circle.start == segment_id {
1111 circle_ctor.start = ctor.position;
1112 } else {
1113 circle_ctor.center = ctor.position;
1114 }
1115 final_edits.insert(owner_id, SegmentCtor::Circle(circle_ctor));
1116 } else {
1117 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1118 "Internal: Circle does not have circle ctor, but found {}",
1119 circle.ctor.human_friendly_kind_with_article()
1120 ))));
1121 }
1122 continue;
1123 }
1124 Segment::ControlPointSpline(spline) if spline.controls.contains(&segment_id) => {
1125 let Some(control_index) =
1126 spline.controls.iter().position(|control_id| *control_id == segment_id)
1127 else {
1128 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1129 "Internal: Point is not part of owner's controlPointSpline segment: point={segment_id:?}, spline={owner_id:?}"
1130 ))));
1131 };
1132 if let Some(existing) = final_edits.get_mut(&owner_id) {
1133 let SegmentCtor::ControlPointSpline(spline_ctor) = existing else {
1134 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1135 "Internal: Expected controlPointSpline ctor for owner, but found {}",
1136 existing.human_friendly_kind_with_article()
1137 ))));
1138 };
1139 spline_ctor.points[control_index] = ctor.position;
1140 } else if let SegmentCtor::ControlPointSpline(spline_ctor) = &spline.ctor {
1141 let mut spline_ctor = spline_ctor.clone();
1142 spline_ctor.points[control_index] = ctor.position;
1143 final_edits.insert(owner_id, SegmentCtor::ControlPointSpline(spline_ctor));
1144 } else {
1145 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1146 "Internal: Control point spline does not have controlPointSpline ctor, but found {}",
1147 spline.ctor.human_friendly_kind_with_article()
1148 ))));
1149 }
1150 continue;
1151 }
1152 _ => {}
1153 }
1154 }
1155
1156 final_edits.insert(segment_id, SegmentCtor::Point(ctor));
1158 }
1159 SegmentCtor::Line(ctor) => {
1160 final_edits.insert(segment_id, SegmentCtor::Line(ctor));
1161 }
1162 SegmentCtor::Arc(ctor) => {
1163 final_edits.insert(segment_id, SegmentCtor::Arc(ctor));
1164 }
1165 SegmentCtor::Circle(ctor) => {
1166 final_edits.insert(segment_id, SegmentCtor::Circle(ctor));
1167 }
1168 SegmentCtor::ControlPointSpline(ctor) => {
1169 final_edits.insert(segment_id, SegmentCtor::ControlPointSpline(ctor));
1170 }
1171 }
1172 }
1173
1174 for (segment_id, ctor) in final_edits {
1175 match ctor {
1176 SegmentCtor::Point(ctor) => self
1177 .edit_point(&mut new_ast, sketch, segment_id, ctor)
1178 .map_err(KclErrorWithOutputs::no_outputs)?,
1179 SegmentCtor::Line(ctor) => self
1180 .edit_line(&mut new_ast, sketch, segment_id, ctor)
1181 .map_err(KclErrorWithOutputs::no_outputs)?,
1182 SegmentCtor::Arc(ctor) => self
1183 .edit_arc(&mut new_ast, sketch, segment_id, ctor)
1184 .map_err(KclErrorWithOutputs::no_outputs)?,
1185 SegmentCtor::Circle(ctor) => self
1186 .edit_circle(&mut new_ast, sketch, segment_id, ctor)
1187 .map_err(KclErrorWithOutputs::no_outputs)?,
1188 SegmentCtor::ControlPointSpline(ctor) => self
1189 .edit_control_point_spline(&mut new_ast, sketch, segment_id, ctor)
1190 .map_err(KclErrorWithOutputs::no_outputs)?,
1191 }
1192 }
1193 for edit in constraint_label_edits {
1194 self.mutate_constraint_label_position(&mut new_ast, edit.constraint_id, edit.label_position)
1195 .map_err(KclErrorWithOutputs::no_outputs)?;
1196 }
1197 let (source_delta, mut scene_graph_delta) = self
1198 .execute_after_edit(
1199 ctx,
1200 sketch,
1201 sketch_block_ref,
1202 &mut new_ast,
1203 ExecuteAfterEditOptions {
1204 segment_ids_edited: drag_anchor_segment_ids,
1205 edit_kind: EditDeleteKind::Edit,
1206 commit_solved_initial_guesses,
1207 },
1208 )
1209 .await?;
1210 if invalidates_ids {
1211 scene_graph_delta.invalidates_ids = true;
1212 }
1213 Ok((source_delta, scene_graph_delta))
1214 }
1215
1216 async fn delete_objects(
1217 &mut self,
1218 ctx: &ExecutorContext,
1219 _version: Version,
1220 sketch: ObjectId,
1221 constraint_ids: Vec<ObjectId>,
1222 segment_ids: Vec<ObjectId>,
1223 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1224 let sketch_block_ref =
1226 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1227
1228 let mut constraint_ids_set = constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
1230 let segment_ids_set = segment_ids.into_iter().collect::<AhashIndexSet<_>>();
1231
1232 let mut resolved_segment_ids_to_delete = AhashIndexSet::default();
1235
1236 for segment_id in segment_ids_set.iter().copied() {
1237 let owner_id = self.scene_graph.objects.get(segment_id.0).and_then(|segment_object| {
1238 let ObjectKind::Segment { segment } = &segment_object.kind else {
1239 return None;
1240 };
1241 match segment {
1242 Segment::Point(point) => point.owner,
1243 Segment::Line(line) => line.owner,
1244 _ => None,
1245 }
1246 });
1247
1248 if let Some(owner_id) = owner_id
1249 && let Some(owner_object) = self.scene_graph.objects.get(owner_id.0)
1250 && let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind
1251 && matches!(
1252 owner_segment,
1253 Segment::Line(_) | Segment::Arc(_) | Segment::Circle(_) | Segment::ControlPointSpline(_)
1254 )
1255 {
1256 resolved_segment_ids_to_delete.insert(owner_id);
1258 } else {
1259 resolved_segment_ids_to_delete.insert(segment_id);
1261 }
1262 }
1263 let referenced_constraint_ids = self
1264 .find_referenced_constraints(sketch, &resolved_segment_ids_to_delete)
1265 .map_err(KclErrorWithOutputs::no_outputs)?;
1266
1267 let mut new_ast = self.program.ast.clone();
1268
1269 for constraint_id in referenced_constraint_ids {
1270 if constraint_ids_set.contains(&constraint_id) {
1271 continue;
1272 }
1273
1274 let constraint_object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
1275 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Constraint not found: {constraint_id:?}")))
1276 })?;
1277 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
1278 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1279 "Object is not a constraint, it is {}",
1280 constraint_object.kind.human_friendly_kind_with_article()
1281 ))));
1282 };
1283
1284 match constraint {
1285 Constraint::Coincident(coincident) => {
1286 let remaining_segments =
1287 self.remaining_constraint_segments(&coincident.segments, &resolved_segment_ids_to_delete);
1288
1289 if remaining_segments.len() >= 2 {
1291 self.edit_coincident_constraint(&mut new_ast, constraint_id, remaining_segments)
1292 .map_err(KclErrorWithOutputs::no_outputs)?;
1293 } else {
1294 constraint_ids_set.insert(constraint_id);
1295 }
1296 }
1297 Constraint::EqualRadius(equal_radius) => {
1298 let remaining_input = equal_radius
1299 .input
1300 .iter()
1301 .copied()
1302 .filter(|segment_id| {
1303 !self.segment_will_be_deleted(*segment_id, &resolved_segment_ids_to_delete)
1304 })
1305 .collect::<Vec<_>>();
1306
1307 if remaining_input.len() >= 2 {
1308 self.edit_equal_radius_constraint(&mut new_ast, constraint_id, remaining_input)
1309 .map_err(KclErrorWithOutputs::no_outputs)?;
1310 } else {
1311 constraint_ids_set.insert(constraint_id);
1312 }
1313 }
1314 Constraint::LinesEqualLength(lines_equal_length) => {
1315 let remaining_lines = lines_equal_length
1316 .lines
1317 .iter()
1318 .copied()
1319 .filter(|line_id| !self.segment_will_be_deleted(*line_id, &resolved_segment_ids_to_delete))
1320 .collect::<Vec<_>>();
1321
1322 if remaining_lines.len() >= 2 {
1324 self.edit_equal_length_constraint(&mut new_ast, constraint_id, remaining_lines)
1325 .map_err(KclErrorWithOutputs::no_outputs)?;
1326 } else {
1327 constraint_ids_set.insert(constraint_id);
1328 }
1329 }
1330 Constraint::Parallel(parallel) => {
1331 let remaining_lines = parallel
1332 .lines
1333 .iter()
1334 .copied()
1335 .filter(|line_id| !self.segment_will_be_deleted(*line_id, &resolved_segment_ids_to_delete))
1336 .collect::<Vec<_>>();
1337
1338 if remaining_lines.len() >= 2 {
1339 self.edit_parallel_constraint(&mut new_ast, constraint_id, remaining_lines)
1340 .map_err(KclErrorWithOutputs::no_outputs)?;
1341 } else {
1342 constraint_ids_set.insert(constraint_id);
1343 }
1344 }
1345 Constraint::Horizontal(Horizontal::Points { points }) => {
1346 let remaining_points = self.remaining_constraint_segments(points, &resolved_segment_ids_to_delete);
1347
1348 if remaining_points.len() >= 2 {
1349 self.edit_horizontal_points_constraint(&mut new_ast, constraint_id, remaining_points)
1350 .map_err(KclErrorWithOutputs::no_outputs)?;
1351 } else {
1352 constraint_ids_set.insert(constraint_id);
1353 }
1354 }
1355 Constraint::Vertical(Vertical::Points { points }) => {
1356 let remaining_points = self.remaining_constraint_segments(points, &resolved_segment_ids_to_delete);
1357
1358 if remaining_points.len() >= 2 {
1359 self.edit_vertical_points_constraint(&mut new_ast, constraint_id, remaining_points)
1360 .map_err(KclErrorWithOutputs::no_outputs)?;
1361 } else {
1362 constraint_ids_set.insert(constraint_id);
1363 }
1364 }
1365 Constraint::Fixed(fixed) => {
1366 if fixed.points.iter().any(|fixed_point| {
1367 self.segment_will_be_deleted(fixed_point.point, &resolved_segment_ids_to_delete)
1368 }) {
1369 constraint_ids_set.insert(constraint_id);
1370 }
1371 }
1372 _ => {
1373 constraint_ids_set.insert(constraint_id);
1375 }
1376 }
1377 }
1378
1379 for constraint_id in constraint_ids_set {
1380 self.delete_constraint(&mut new_ast, sketch, constraint_id)
1381 .map_err(KclErrorWithOutputs::no_outputs)?;
1382 }
1383 for segment_id in resolved_segment_ids_to_delete {
1384 self.delete_segment(&mut new_ast, sketch, segment_id)
1385 .map_err(KclErrorWithOutputs::no_outputs)?;
1386 }
1387
1388 self.execute_after_edit(
1389 ctx,
1390 sketch,
1391 sketch_block_ref,
1392 &mut new_ast,
1393 ExecuteAfterEditOptions {
1394 segment_ids_edited: Default::default(),
1395 edit_kind: EditDeleteKind::DeleteNonSketch,
1396 commit_solved_initial_guesses: true,
1397 },
1398 )
1399 .await
1400 }
1401
1402 async fn add_constraint(
1403 &mut self,
1404 ctx: &ExecutorContext,
1405 _version: Version,
1406 sketch: ObjectId,
1407 constraint: Constraint,
1408 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1409 let original_program = self.program.clone();
1413 let original_scene_graph = self.scene_graph.clone();
1414
1415 let mut new_ast = self.program.ast.clone();
1416 let sketch_block_ref = match constraint {
1417 Constraint::Coincident(coincident) => self
1418 .add_coincident(sketch, coincident, &mut new_ast)
1419 .await
1420 .map_err(KclErrorWithOutputs::no_outputs)?,
1421 Constraint::Distance(distance) => self
1422 .add_distance(sketch, distance, &mut new_ast)
1423 .await
1424 .map_err(KclErrorWithOutputs::no_outputs)?,
1425 Constraint::EqualRadius(equal_radius) => self
1426 .add_equal_radius(sketch, equal_radius, &mut new_ast)
1427 .await
1428 .map_err(KclErrorWithOutputs::no_outputs)?,
1429 Constraint::Fixed(fixed) => self
1430 .add_fixed_constraints(sketch, fixed.points, &mut new_ast)
1431 .await
1432 .map_err(KclErrorWithOutputs::no_outputs)?,
1433 Constraint::HorizontalDistance(distance) => self
1434 .add_horizontal_distance(sketch, distance, &mut new_ast)
1435 .await
1436 .map_err(KclErrorWithOutputs::no_outputs)?,
1437 Constraint::VerticalDistance(distance) => self
1438 .add_vertical_distance(sketch, distance, &mut new_ast)
1439 .await
1440 .map_err(KclErrorWithOutputs::no_outputs)?,
1441 Constraint::Horizontal(horizontal) => self
1442 .add_horizontal(sketch, horizontal, &mut new_ast)
1443 .await
1444 .map_err(KclErrorWithOutputs::no_outputs)?,
1445 Constraint::LinesEqualLength(lines_equal_length) => self
1446 .add_lines_equal_length(sketch, lines_equal_length, &mut new_ast)
1447 .await
1448 .map_err(KclErrorWithOutputs::no_outputs)?,
1449 Constraint::Midpoint(midpoint) => self
1450 .add_midpoint(sketch, midpoint, &mut new_ast)
1451 .await
1452 .map_err(KclErrorWithOutputs::no_outputs)?,
1453 Constraint::Parallel(parallel) => self
1454 .add_parallel(sketch, parallel, &mut new_ast)
1455 .await
1456 .map_err(KclErrorWithOutputs::no_outputs)?,
1457 Constraint::Perpendicular(perpendicular) => self
1458 .add_perpendicular(sketch, perpendicular, &mut new_ast)
1459 .await
1460 .map_err(KclErrorWithOutputs::no_outputs)?,
1461 Constraint::Radius(radius) => self
1462 .add_radius(sketch, radius, &mut new_ast)
1463 .await
1464 .map_err(KclErrorWithOutputs::no_outputs)?,
1465 Constraint::Diameter(diameter) => self
1466 .add_diameter(sketch, diameter, &mut new_ast)
1467 .await
1468 .map_err(KclErrorWithOutputs::no_outputs)?,
1469 Constraint::Symmetric(symmetric) => self
1470 .add_symmetric(sketch, symmetric, &mut new_ast)
1471 .await
1472 .map_err(KclErrorWithOutputs::no_outputs)?,
1473 Constraint::Vertical(vertical) => self
1474 .add_vertical(sketch, vertical, &mut new_ast)
1475 .await
1476 .map_err(KclErrorWithOutputs::no_outputs)?,
1477 Constraint::Angle(lines_at_angle) => self
1478 .add_angle(sketch, lines_at_angle, &mut new_ast)
1479 .await
1480 .map_err(KclErrorWithOutputs::no_outputs)?,
1481 Constraint::Tangent(tangent) => self
1482 .add_tangent(sketch, tangent, &mut new_ast)
1483 .await
1484 .map_err(KclErrorWithOutputs::no_outputs)?,
1485 };
1486
1487 let result = self
1488 .execute_after_add_constraint(ctx, sketch, sketch_block_ref, &mut new_ast)
1489 .await;
1490
1491 if result.is_err() {
1493 self.program = original_program;
1494 self.scene_graph = original_scene_graph;
1495 }
1496
1497 result
1498 }
1499
1500 async fn chain_segment(
1501 &mut self,
1502 ctx: &ExecutorContext,
1503 version: Version,
1504 sketch: ObjectId,
1505 previous_segment_end_point_id: ObjectId,
1506 segment: SegmentCtor,
1507 _label: Option<String>,
1508 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1509 let SegmentCtor::Line(line_ctor) = segment else {
1513 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1514 "chain_segment currently only supports Line segments, got {}",
1515 segment.human_friendly_kind_with_article(),
1516 ))));
1517 };
1518
1519 let (_first_src_delta, first_scene_delta) = self.add_line(ctx, sketch, line_ctor).await?;
1521
1522 let new_line_id = first_scene_delta
1525 .new_objects
1526 .iter()
1527 .find(|&obj_id| {
1528 let obj = self.scene_graph.objects.get(obj_id.0);
1529 if let Some(obj) = obj {
1530 matches!(
1531 &obj.kind,
1532 ObjectKind::Segment {
1533 segment: Segment::Line(_)
1534 }
1535 )
1536 } else {
1537 false
1538 }
1539 })
1540 .ok_or_else(|| {
1541 KclErrorWithOutputs::no_outputs(KclError::refactor(
1542 "Failed to find new line segment in scene graph".to_string(),
1543 ))
1544 })?;
1545
1546 let new_line_obj = self.scene_graph.objects.get(new_line_id.0).ok_or_else(|| {
1547 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1548 "New line object not found: {new_line_id:?}"
1549 )))
1550 })?;
1551
1552 let ObjectKind::Segment {
1553 segment: new_line_segment,
1554 } = &new_line_obj.kind
1555 else {
1556 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1557 "Object is not a segment: {new_line_obj:?}"
1558 ))));
1559 };
1560
1561 let Segment::Line(new_line) = new_line_segment else {
1562 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1563 "Segment is not a line: {new_line_segment:?}"
1564 ))));
1565 };
1566
1567 let new_line_start_point_id = new_line.start;
1568
1569 let coincident = Coincident {
1571 segments: vec![previous_segment_end_point_id.into(), new_line_start_point_id.into()],
1572 };
1573
1574 let (final_src_delta, final_scene_delta) = self
1575 .add_constraint(ctx, version, sketch, Constraint::Coincident(coincident))
1576 .await?;
1577
1578 let mut combined_new_objects = first_scene_delta.new_objects.clone();
1581 combined_new_objects.extend(final_scene_delta.new_objects);
1582
1583 let scene_graph_delta = SceneGraphDelta {
1584 new_graph: self.scene_graph_for_ui(),
1585 invalidates_ids: false,
1586 new_objects: combined_new_objects,
1587 exec_outcome: final_scene_delta.exec_outcome,
1588 };
1589
1590 Ok((final_src_delta, scene_graph_delta))
1591 }
1592
1593 async fn edit_constraint_value(
1595 &mut self,
1596 ctx: &ExecutorContext,
1597 _version: Version,
1598 sketch: ObjectId,
1599 constraint_id: ObjectId,
1600 value_expression: String,
1601 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1602 let sketch_block_ref =
1604 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1605
1606 let object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
1607 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Object not found: {constraint_id:?}")))
1608 })?;
1609 if !matches!(&object.kind, ObjectKind::Constraint { .. }) {
1610 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1611 "Object is not a constraint: {constraint_id:?}"
1612 ))));
1613 }
1614
1615 let mut new_ast = self.program.ast.clone();
1616
1617 let (parsed, errors) = Program::parse(&value_expression).map_err(|e| {
1619 KclErrorWithOutputs::no_outputs(KclError::refactor(format_kcl_error_message(
1620 "Invalid constraint value",
1621 &e,
1622 )))
1623 })?;
1624 if !errors.is_empty() {
1625 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1626 format_compilation_issues("Invalid constraint value", &errors),
1627 )));
1628 }
1629 let mut parsed = parsed.ok_or_else(|| {
1630 KclErrorWithOutputs::no_outputs(KclError::refactor("No AST produced from value expression".to_string()))
1631 })?;
1632 if parsed.ast.body.is_empty() {
1633 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1634 "Empty value expression".to_string(),
1635 )));
1636 }
1637 let first = parsed.ast.body.remove(0);
1638 let ast::BodyItem::ExpressionStatement(expr_stmt) = first else {
1639 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1640 "Value expression must be a simple expression".to_string(),
1641 )));
1642 };
1643
1644 let new_value: ast::BinaryPart = expr_stmt
1645 .inner
1646 .expression
1647 .try_into()
1648 .map_err(|e: String| KclErrorWithOutputs::no_outputs(KclError::refactor(e)))?;
1649
1650 self.mutate_ast(
1651 &mut new_ast,
1652 constraint_id,
1653 AstMutateCommand::EditConstraintValue { value: new_value },
1654 )
1655 .map_err(KclErrorWithOutputs::no_outputs)?;
1656
1657 self.execute_after_edit(
1658 ctx,
1659 sketch,
1660 sketch_block_ref,
1661 &mut new_ast,
1662 ExecuteAfterEditOptions {
1663 segment_ids_edited: Default::default(),
1664 edit_kind: EditDeleteKind::Edit,
1665 commit_solved_initial_guesses: true,
1666 },
1667 )
1668 .await
1669 }
1670
1671 async fn edit_distance_constraint_label_position(
1672 &mut self,
1673 ctx: &ExecutorContext,
1674 _version: Version,
1675 sketch: ObjectId,
1676 constraint_id: ObjectId,
1677 label_position: Point2d<Number>,
1678 anchor_segment_ids: Vec<ObjectId>,
1679 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1680 let sketch_block_ref =
1682 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1683
1684 let mut new_ast = self.program.ast.clone();
1685 self.mutate_constraint_label_position(&mut new_ast, constraint_id, label_position)
1686 .map_err(KclErrorWithOutputs::no_outputs)?;
1687 let commit_solved_initial_guesses = self.next_edit_commits_solver_solutions.take().unwrap_or(true);
1688
1689 self.execute_after_edit(
1690 ctx,
1691 sketch,
1692 sketch_block_ref,
1693 &mut new_ast,
1694 ExecuteAfterEditOptions {
1695 segment_ids_edited: anchor_segment_ids.into_iter().collect(),
1696 edit_kind: EditDeleteKind::Edit,
1697 commit_solved_initial_guesses,
1698 },
1699 )
1700 .await
1701 }
1702
1703 async fn batch_split_segment_operations(
1711 &mut self,
1712 ctx: &ExecutorContext,
1713 _version: Version,
1714 sketch: ObjectId,
1715 edit_segments: Vec<ExistingSegmentCtor>,
1716 add_constraints: Vec<Constraint>,
1717 delete_constraint_ids: Vec<ObjectId>,
1718 _new_segment_info: sketch::NewSegmentInfo,
1719 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1720 let sketch_block_ref =
1722 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1723
1724 let mut new_ast = self.program.ast.clone();
1725 let mut segment_ids_edited = AhashIndexSet::with_capacity_and_hasher(edit_segments.len(), Default::default());
1726
1727 for segment in edit_segments {
1729 segment_ids_edited.insert(segment.id);
1730 match segment.ctor {
1731 SegmentCtor::Point(ctor) => self
1732 .edit_point(&mut new_ast, sketch, segment.id, ctor)
1733 .map_err(KclErrorWithOutputs::no_outputs)?,
1734 SegmentCtor::Line(ctor) => self
1735 .edit_line(&mut new_ast, sketch, segment.id, ctor)
1736 .map_err(KclErrorWithOutputs::no_outputs)?,
1737 SegmentCtor::Arc(ctor) => self
1738 .edit_arc(&mut new_ast, sketch, segment.id, ctor)
1739 .map_err(KclErrorWithOutputs::no_outputs)?,
1740 SegmentCtor::Circle(ctor) => self
1741 .edit_circle(&mut new_ast, sketch, segment.id, ctor)
1742 .map_err(KclErrorWithOutputs::no_outputs)?,
1743 SegmentCtor::ControlPointSpline(ctor) => self
1744 .edit_control_point_spline(&mut new_ast, sketch, segment.id, ctor)
1745 .map_err(KclErrorWithOutputs::no_outputs)?,
1746 }
1747 }
1748
1749 for constraint in add_constraints {
1751 match constraint {
1752 Constraint::Coincident(coincident) => {
1753 self.add_coincident(sketch, coincident, &mut new_ast)
1754 .await
1755 .map_err(KclErrorWithOutputs::no_outputs)?;
1756 }
1757 Constraint::Distance(distance) => {
1758 self.add_distance(sketch, distance, &mut new_ast)
1759 .await
1760 .map_err(KclErrorWithOutputs::no_outputs)?;
1761 }
1762 Constraint::EqualRadius(equal_radius) => {
1763 self.add_equal_radius(sketch, equal_radius, &mut new_ast)
1764 .await
1765 .map_err(KclErrorWithOutputs::no_outputs)?;
1766 }
1767 Constraint::Fixed(fixed) => {
1768 self.add_fixed_constraints(sketch, fixed.points, &mut new_ast)
1769 .await
1770 .map_err(KclErrorWithOutputs::no_outputs)?;
1771 }
1772 Constraint::HorizontalDistance(distance) => {
1773 self.add_horizontal_distance(sketch, distance, &mut new_ast)
1774 .await
1775 .map_err(KclErrorWithOutputs::no_outputs)?;
1776 }
1777 Constraint::VerticalDistance(distance) => {
1778 self.add_vertical_distance(sketch, distance, &mut new_ast)
1779 .await
1780 .map_err(KclErrorWithOutputs::no_outputs)?;
1781 }
1782 Constraint::Horizontal(horizontal) => {
1783 self.add_horizontal(sketch, horizontal, &mut new_ast)
1784 .await
1785 .map_err(KclErrorWithOutputs::no_outputs)?;
1786 }
1787 Constraint::LinesEqualLength(lines_equal_length) => {
1788 self.add_lines_equal_length(sketch, lines_equal_length, &mut new_ast)
1789 .await
1790 .map_err(KclErrorWithOutputs::no_outputs)?;
1791 }
1792 Constraint::Midpoint(midpoint) => {
1793 self.add_midpoint(sketch, midpoint, &mut new_ast)
1794 .await
1795 .map_err(KclErrorWithOutputs::no_outputs)?;
1796 }
1797 Constraint::Parallel(parallel) => {
1798 self.add_parallel(sketch, parallel, &mut new_ast)
1799 .await
1800 .map_err(KclErrorWithOutputs::no_outputs)?;
1801 }
1802 Constraint::Perpendicular(perpendicular) => {
1803 self.add_perpendicular(sketch, perpendicular, &mut new_ast)
1804 .await
1805 .map_err(KclErrorWithOutputs::no_outputs)?;
1806 }
1807 Constraint::Vertical(vertical) => {
1808 self.add_vertical(sketch, vertical, &mut new_ast)
1809 .await
1810 .map_err(KclErrorWithOutputs::no_outputs)?;
1811 }
1812 Constraint::Diameter(diameter) => {
1813 self.add_diameter(sketch, diameter, &mut new_ast)
1814 .await
1815 .map_err(KclErrorWithOutputs::no_outputs)?;
1816 }
1817 Constraint::Radius(radius) => {
1818 self.add_radius(sketch, radius, &mut new_ast)
1819 .await
1820 .map_err(KclErrorWithOutputs::no_outputs)?;
1821 }
1822 Constraint::Symmetric(symmetric) => {
1823 self.add_symmetric(sketch, symmetric, &mut new_ast)
1824 .await
1825 .map_err(KclErrorWithOutputs::no_outputs)?;
1826 }
1827 Constraint::Angle(angle) => {
1828 self.add_angle(sketch, angle, &mut new_ast)
1829 .await
1830 .map_err(KclErrorWithOutputs::no_outputs)?;
1831 }
1832 Constraint::Tangent(tangent) => {
1833 self.add_tangent(sketch, tangent, &mut new_ast)
1834 .await
1835 .map_err(KclErrorWithOutputs::no_outputs)?;
1836 }
1837 }
1838 }
1839
1840 let constraint_ids_set = delete_constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
1842
1843 let has_constraint_deletions = !constraint_ids_set.is_empty();
1844 for constraint_id in constraint_ids_set {
1845 self.delete_constraint(&mut new_ast, sketch, constraint_id)
1846 .map_err(KclErrorWithOutputs::no_outputs)?;
1847 }
1848
1849 let (source_delta, mut scene_graph_delta) = self
1853 .execute_after_edit(
1854 ctx,
1855 sketch,
1856 sketch_block_ref,
1857 &mut new_ast,
1858 ExecuteAfterEditOptions {
1859 segment_ids_edited,
1860 edit_kind: EditDeleteKind::Edit,
1861 commit_solved_initial_guesses: true,
1862 },
1863 )
1864 .await?;
1865
1866 if has_constraint_deletions {
1869 scene_graph_delta.invalidates_ids = true;
1870 }
1871
1872 Ok((source_delta, scene_graph_delta))
1873 }
1874
1875 async fn batch_tail_cut_operations(
1876 &mut self,
1877 ctx: &ExecutorContext,
1878 _version: Version,
1879 sketch: ObjectId,
1880 edit_segments: Vec<ExistingSegmentCtor>,
1881 add_constraints: Vec<Constraint>,
1882 delete_constraint_ids: Vec<ObjectId>,
1883 additional_edited_segment_ids: Vec<ObjectId>,
1884 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1885 let sketch_block_ref =
1886 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1887
1888 let mut new_ast = self.program.ast.clone();
1889 let mut segment_ids_edited = AhashIndexSet::with_capacity_and_hasher(edit_segments.len(), Default::default());
1890
1891 for segment in edit_segments {
1893 segment_ids_edited.insert(segment.id);
1894 match segment.ctor {
1895 SegmentCtor::Point(ctor) => self
1896 .edit_point(&mut new_ast, sketch, segment.id, ctor)
1897 .map_err(KclErrorWithOutputs::no_outputs)?,
1898 SegmentCtor::Line(ctor) => self
1899 .edit_line(&mut new_ast, sketch, segment.id, ctor)
1900 .map_err(KclErrorWithOutputs::no_outputs)?,
1901 SegmentCtor::Arc(ctor) => self
1902 .edit_arc(&mut new_ast, sketch, segment.id, ctor)
1903 .map_err(KclErrorWithOutputs::no_outputs)?,
1904 SegmentCtor::Circle(ctor) => self
1905 .edit_circle(&mut new_ast, sketch, segment.id, ctor)
1906 .map_err(KclErrorWithOutputs::no_outputs)?,
1907 SegmentCtor::ControlPointSpline(ctor) => self
1908 .edit_control_point_spline(&mut new_ast, sketch, segment.id, ctor)
1909 .map_err(KclErrorWithOutputs::no_outputs)?,
1910 }
1911 }
1912
1913 segment_ids_edited.extend(additional_edited_segment_ids);
1914
1915 for constraint in add_constraints {
1917 match constraint {
1918 Constraint::Coincident(coincident) => {
1919 self.add_coincident(sketch, coincident, &mut new_ast)
1920 .await
1921 .map_err(KclErrorWithOutputs::no_outputs)?;
1922 }
1923 other => {
1924 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1925 "unsupported constraint in tail cut batch: {other:?}"
1926 ))));
1927 }
1928 }
1929 }
1930
1931 let constraint_ids_set = delete_constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
1933
1934 let has_constraint_deletions = !constraint_ids_set.is_empty();
1935 for constraint_id in constraint_ids_set {
1936 self.delete_constraint(&mut new_ast, sketch, constraint_id)
1937 .map_err(KclErrorWithOutputs::no_outputs)?;
1938 }
1939
1940 let (source_delta, mut scene_graph_delta) = self
1944 .execute_after_edit(
1945 ctx,
1946 sketch,
1947 sketch_block_ref,
1948 &mut new_ast,
1949 ExecuteAfterEditOptions {
1950 segment_ids_edited,
1951 edit_kind: EditDeleteKind::Edit,
1952 commit_solved_initial_guesses: true,
1953 },
1954 )
1955 .await?;
1956
1957 if has_constraint_deletions {
1960 scene_graph_delta.invalidates_ids = true;
1961 }
1962
1963 Ok((source_delta, scene_graph_delta))
1964 }
1965}
1966
1967impl FrontendState {
1968 pub async fn evaluate_expression(&self, ctx: &ExecutorContext, mut program: Program) -> ExecResult<ExecOutcome> {
1970 program.ast.inner_attrs.clone_from(&self.program.ast.inner_attrs);
1971 ctx.run_mock(&program, &MockConfig::default()).await
1972 }
1973
1974 pub async fn hack_set_program(&mut self, ctx: &ExecutorContext, program: Program) -> ExecResult<SetProgramOutcome> {
1975 self.program = program.clone();
1976
1977 self.point_freedom_cache.clear();
1988 match ctx.run_with_caching(program).await {
1989 Ok(outcome) => {
1990 let outcome = self.update_state_after_exec(outcome, true);
1991 let checkpoint_id = self
1992 .create_sketch_checkpoint(outcome.clone())
1993 .await
1994 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
1995 Ok(SetProgramOutcome::Success {
1996 scene_graph: Box::new(self.scene_graph_for_ui()),
1997 exec_outcome: Box::new(outcome),
1998 checkpoint_id: Some(checkpoint_id),
1999 })
2000 }
2001 Err(mut err) => {
2002 let outcome = self.exec_outcome_from_exec_error(err.clone())?;
2005 self.update_state_after_exec(outcome, true);
2006 err.scene_graph = Some(self.scene_graph_for_ui());
2007 Ok(SetProgramOutcome::ExecFailure { error: Box::new(err) })
2008 }
2009 }
2010 }
2011
2012 pub async fn engine_execute(
2015 &mut self,
2016 ctx: &ExecutorContext,
2017 program: Program,
2018 ) -> Result<SceneGraphDelta, KclErrorWithOutputs> {
2019 self.program = program.clone();
2020
2021 self.point_freedom_cache.clear();
2025 match ctx.run_with_caching(program).await {
2026 Ok(outcome) => {
2027 let outcome = self.update_state_after_exec(outcome, true);
2028 Ok(SceneGraphDelta {
2029 new_graph: self.scene_graph_for_ui(),
2030 exec_outcome: outcome,
2031 new_objects: Default::default(),
2033 invalidates_ids: Default::default(),
2035 })
2036 }
2037 Err(mut err) => {
2038 let outcome = self.exec_outcome_from_exec_error(err.clone())?;
2040 self.update_state_after_exec(outcome, true);
2041 err.scene_graph = Some(self.scene_graph_for_ui());
2042 Err(err)
2043 }
2044 }
2045 }
2046
2047 fn exec_outcome_from_exec_error(&self, err: KclErrorWithOutputs) -> Result<ExecOutcome, KclErrorWithOutputs> {
2048 if matches!(err.error, KclError::EngineHangup { .. }) {
2049 return Err(err);
2053 }
2054
2055 let KclErrorWithOutputs {
2056 error,
2057 mut non_fatal,
2058 variables,
2059 operations,
2060 artifact_graph,
2061 scene_objects,
2062 source_range_to_object,
2063 var_solutions,
2064 refactor_metadata,
2065 filenames,
2066 source_files,
2067 default_planes,
2068 ..
2069 } = err;
2070
2071 non_fatal.push(CompilationIssue::fatal(issue_source_range(&error), error.get_message()));
2072
2073 Ok(ExecOutcome {
2074 variables,
2075 #[cfg(test)]
2076 test_program_memory: Default::default(),
2077 filenames,
2078 operations,
2079 artifact_graph,
2080 scene_objects,
2081 source_range_to_object,
2082 var_solutions,
2083 refactor_metadata,
2084 issues: non_fatal,
2085 source_files,
2086 default_planes,
2087 })
2088 }
2089
2090 async fn add_point(
2091 &mut self,
2092 ctx: &ExecutorContext,
2093 sketch: ObjectId,
2094 ctor: PointCtor,
2095 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2096 let at_ast = to_ast_point2d(&ctor.position)
2098 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2099 let point_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
2100 callee: ast::Node::no_src(ast_sketch2_name(POINT_FN)),
2101 unlabeled: None,
2102 arguments: vec![ast::LabeledArg {
2103 label: Some(ast::Identifier::new(POINT_AT_PARAM)),
2104 arg: at_ast,
2105 }],
2106 digest: None,
2107 non_code_meta: Default::default(),
2108 })));
2109
2110 let sketch_id = sketch;
2112 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2113 #[cfg(target_arch = "wasm32")]
2114 web_sys::console::error_1(
2115 &format!(
2116 "Sketch not found; sketch_id={sketch_id:?}, self.scene_graph.objects={:#?}",
2117 self.scene_graph.objects
2118 )
2119 .into(),
2120 );
2121 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2122 })?;
2123 let ObjectKind::Sketch(_) = &sketch_object.kind else {
2124 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2125 "Object is not a sketch, it is {}",
2126 sketch_object.kind.human_friendly_kind_with_article(),
2127 ))));
2128 };
2129 let mut new_ast = self.program.ast.clone();
2131 let (sketch_block_ref, _) = self
2132 .mutate_ast(
2133 &mut new_ast,
2134 sketch_id,
2135 AstMutateCommand::AddSketchBlockExprStmt { expr: point_ast },
2136 )
2137 .map_err(KclErrorWithOutputs::no_outputs)?;
2138 let new_source = source_from_ast(&new_ast);
2140 let new_program = parse_frontend_mutation_source(
2142 &new_source,
2143 "Error parsing KCL source after adding point",
2144 "No AST produced after adding point",
2145 )?;
2146
2147 let point_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2148 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2149 "Source range of point not found in sketch block: {sketch_block_ref:?}; {err:?}"
2150 )))
2151 })?;
2152
2153 self.program = new_program.clone();
2155
2156 let mut truncated_program = new_program;
2158 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2159 .map_err(KclErrorWithOutputs::no_outputs)?;
2160
2161 let outcome = ctx
2163 .run_mock(
2164 &truncated_program,
2165 &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2166 )
2167 .await?;
2168
2169 let new_object_ids = {
2170 let make_err =
2171 |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2172 let segment_id = outcome
2173 .source_range_to_object
2174 .get(&point_node_ref.range)
2175 .copied()
2176 .ok_or_else(|| make_err(format!("Source range of point not found: {point_node_ref:?}")))?;
2177 let segment_object = outcome
2178 .scene_objects
2179 .get(segment_id.0)
2180 .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2181 let ObjectKind::Segment { segment } = &segment_object.kind else {
2182 return Err(make_err(format!(
2183 "Object is not a segment, it is {}",
2184 segment_object.kind.human_friendly_kind_with_article()
2185 )));
2186 };
2187 let Segment::Point(_) = segment else {
2188 return Err(make_err(format!(
2189 "Segment is not a point, it is {}",
2190 segment.human_friendly_kind_with_article()
2191 )));
2192 };
2193 vec![segment_id]
2194 };
2195 let src_delta = SourceDelta { text: new_source };
2196 let outcome = self.update_state_after_exec(outcome, false);
2198 let scene_graph_delta = SceneGraphDelta {
2199 new_graph: self.scene_graph_for_ui(),
2200 invalidates_ids: false,
2201 new_objects: new_object_ids,
2202 exec_outcome: outcome,
2203 };
2204 Ok((src_delta, scene_graph_delta))
2205 }
2206
2207 async fn add_line(
2208 &mut self,
2209 ctx: &ExecutorContext,
2210 sketch: ObjectId,
2211 ctor: LineCtor,
2212 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2213 let start_ast = to_ast_point2d(&ctor.start)
2215 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2216 let end_ast = to_ast_point2d(&ctor.end)
2217 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2218 let mut arguments = vec![
2219 ast::LabeledArg {
2220 label: Some(ast::Identifier::new(LINE_START_PARAM)),
2221 arg: start_ast,
2222 },
2223 ast::LabeledArg {
2224 label: Some(ast::Identifier::new(LINE_END_PARAM)),
2225 arg: end_ast,
2226 },
2227 ];
2228 if ctor.construction == Some(true) {
2230 arguments.push(ast::LabeledArg {
2231 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2232 arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
2233 value: ast::LiteralValue::Bool(true),
2234 raw: "true".to_string(),
2235 digest: None,
2236 }))),
2237 });
2238 }
2239 let line_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
2240 callee: ast::Node::no_src(ast_sketch2_name(LINE_FN)),
2241 unlabeled: None,
2242 arguments,
2243 digest: None,
2244 non_code_meta: Default::default(),
2245 })));
2246
2247 let sketch_id = sketch;
2249 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2250 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2251 })?;
2252 let ObjectKind::Sketch(_) = &sketch_object.kind else {
2253 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2254 "Object is not a sketch, it is {}",
2255 sketch_object.kind.human_friendly_kind_with_article(),
2256 ))));
2257 };
2258 let mut new_ast = self.program.ast.clone();
2260 let (sketch_block_ref, _) = self
2261 .mutate_ast(
2262 &mut new_ast,
2263 sketch_id,
2264 AstMutateCommand::AddSketchBlockExprStmt { expr: line_ast },
2265 )
2266 .map_err(KclErrorWithOutputs::no_outputs)?;
2267 let new_source = source_from_ast(&new_ast);
2269 let new_program = parse_frontend_mutation_source(
2271 &new_source,
2272 "Error parsing KCL source after adding line",
2273 "No AST produced after adding line",
2274 )?;
2275
2276 let line_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2277 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2278 "Source range of line not found in sketch block: {sketch_block_ref:?}; {err:?}"
2279 )))
2280 })?;
2281
2282 self.program = new_program.clone();
2284
2285 let mut truncated_program = new_program;
2287 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2288 .map_err(KclErrorWithOutputs::no_outputs)?;
2289
2290 let outcome = ctx
2292 .run_mock(
2293 &truncated_program,
2294 &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2295 )
2296 .await?;
2297
2298 let new_object_ids = {
2299 let make_err =
2300 |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2301 let segment_id = outcome
2302 .source_range_to_object
2303 .get(&line_node_ref.range)
2304 .copied()
2305 .ok_or_else(|| make_err(format!("Source range of line not found: {line_node_ref:?}")))?;
2306 let segment_object = outcome
2307 .scene_object_by_id(segment_id)
2308 .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2309 let ObjectKind::Segment { segment } = &segment_object.kind else {
2310 return Err(make_err(format!(
2311 "Object is not a segment, it is {}",
2312 segment_object.kind.human_friendly_kind_with_article()
2313 )));
2314 };
2315 let Segment::Line(line) = segment else {
2316 return Err(make_err(format!(
2317 "Segment is not a line, it is {}",
2318 segment.human_friendly_kind_with_article()
2319 )));
2320 };
2321 vec![line.start, line.end, segment_id]
2322 };
2323 let src_delta = SourceDelta { text: new_source };
2324 let outcome = self.update_state_after_exec(outcome, false);
2326 let scene_graph_delta = SceneGraphDelta {
2327 new_graph: self.scene_graph_for_ui(),
2328 invalidates_ids: false,
2329 new_objects: new_object_ids,
2330 exec_outcome: outcome,
2331 };
2332 Ok((src_delta, scene_graph_delta))
2333 }
2334
2335 async fn add_arc(
2336 &mut self,
2337 ctx: &ExecutorContext,
2338 sketch: ObjectId,
2339 ctor: ArcCtor,
2340 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2341 let start_ast = to_ast_point2d(&ctor.start)
2343 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2344 let end_ast = to_ast_point2d(&ctor.end)
2345 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2346 let center_ast = to_ast_point2d(&ctor.center)
2347 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2348 let mut arguments = vec![
2349 ast::LabeledArg {
2350 label: Some(ast::Identifier::new(ARC_START_PARAM)),
2351 arg: start_ast,
2352 },
2353 ast::LabeledArg {
2354 label: Some(ast::Identifier::new(ARC_END_PARAM)),
2355 arg: end_ast,
2356 },
2357 ast::LabeledArg {
2358 label: Some(ast::Identifier::new(ARC_CENTER_PARAM)),
2359 arg: center_ast,
2360 },
2361 ];
2362 if ctor.direction == Some(ArcDirection::Cw) {
2365 arguments.push(ast::LabeledArg {
2366 label: Some(ast::Identifier::new(ARC_DIRECTION_PARAM)),
2367 arg: ast::Expr::Name(BoxNode::new(ast::Name::new(ARC_DIRECTION_CW_NAME))),
2368 });
2369 }
2370 if ctor.construction == Some(true) {
2372 arguments.push(ast::LabeledArg {
2373 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2374 arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
2375 value: ast::LiteralValue::Bool(true),
2376 raw: "true".to_string(),
2377 digest: None,
2378 }))),
2379 });
2380 }
2381 let arc_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
2382 callee: ast::Node::no_src(ast_sketch2_name(ARC_FN)),
2383 unlabeled: None,
2384 arguments,
2385 digest: None,
2386 non_code_meta: Default::default(),
2387 })));
2388
2389 let sketch_id = sketch;
2391 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2392 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2393 })?;
2394 let ObjectKind::Sketch(_) = &sketch_object.kind else {
2395 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2396 "Object is not a sketch, it is {}",
2397 sketch_object.kind.human_friendly_kind_with_article(),
2398 ))));
2399 };
2400 let mut new_ast = self.program.ast.clone();
2402 let (sketch_block_ref, _) = self
2403 .mutate_ast(
2404 &mut new_ast,
2405 sketch_id,
2406 AstMutateCommand::AddSketchBlockExprStmt { expr: arc_ast },
2407 )
2408 .map_err(KclErrorWithOutputs::no_outputs)?;
2409 let new_source = source_from_ast(&new_ast);
2411 let new_program = parse_frontend_mutation_source(
2413 &new_source,
2414 "Error parsing KCL source after adding arc",
2415 "No AST produced after adding arc",
2416 )?;
2417
2418 let arc_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2419 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2420 "Source range of arc not found in sketch block: {sketch_block_ref:?}; {err:?}"
2421 )))
2422 })?;
2423
2424 self.program = new_program.clone();
2426
2427 let mut truncated_program = new_program;
2429 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2430 .map_err(KclErrorWithOutputs::no_outputs)?;
2431
2432 let outcome = ctx
2434 .run_mock(
2435 &truncated_program,
2436 &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2437 )
2438 .await?;
2439
2440 let new_object_ids = {
2441 let make_err =
2442 |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2443 let segment_id = outcome
2444 .source_range_to_object
2445 .get(&arc_node_ref.range)
2446 .copied()
2447 .ok_or_else(|| make_err(format!("Source range of arc not found: {arc_node_ref:?}")))?;
2448 let segment_object = outcome
2449 .scene_objects
2450 .get(segment_id.0)
2451 .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2452 let ObjectKind::Segment { segment } = &segment_object.kind else {
2453 return Err(make_err(format!(
2454 "Object is not a segment, it is {}",
2455 segment_object.kind.human_friendly_kind_with_article()
2456 )));
2457 };
2458 let Segment::Arc(arc) = segment else {
2459 return Err(make_err(format!(
2460 "Segment is not an arc, it is {}",
2461 segment.human_friendly_kind_with_article()
2462 )));
2463 };
2464 vec![arc.start, arc.end, arc.center, segment_id]
2465 };
2466 let src_delta = SourceDelta { text: new_source };
2467 let outcome = self.update_state_after_exec(outcome, false);
2469 let scene_graph_delta = SceneGraphDelta {
2470 new_graph: self.scene_graph_for_ui(),
2471 invalidates_ids: false,
2472 new_objects: new_object_ids,
2473 exec_outcome: outcome,
2474 };
2475 Ok((src_delta, scene_graph_delta))
2476 }
2477
2478 async fn add_circle(
2479 &mut self,
2480 ctx: &ExecutorContext,
2481 sketch: ObjectId,
2482 ctor: CircleCtor,
2483 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2484 let start_ast = to_ast_point2d(&ctor.start)
2486 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2487 let center_ast = to_ast_point2d(&ctor.center)
2488 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2489 let mut arguments = vec![
2490 ast::LabeledArg {
2491 label: Some(ast::Identifier::new(CIRCLE_START_PARAM)),
2492 arg: start_ast,
2493 },
2494 ast::LabeledArg {
2495 label: Some(ast::Identifier::new(CIRCLE_CENTER_PARAM)),
2496 arg: center_ast,
2497 },
2498 ];
2499 if ctor.construction == Some(true) {
2501 arguments.push(ast::LabeledArg {
2502 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2503 arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
2504 value: ast::LiteralValue::Bool(true),
2505 raw: "true".to_string(),
2506 digest: None,
2507 }))),
2508 });
2509 }
2510 let circle_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
2511 callee: ast::Node::no_src(ast_sketch2_name(CIRCLE_FN)),
2512 unlabeled: None,
2513 arguments,
2514 digest: None,
2515 non_code_meta: Default::default(),
2516 })));
2517
2518 let sketch_id = sketch;
2520 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2521 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2522 })?;
2523 let ObjectKind::Sketch(_) = &sketch_object.kind else {
2524 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2525 "Object is not a sketch, it is {}",
2526 sketch_object.kind.human_friendly_kind_with_article(),
2527 ))));
2528 };
2529 let mut new_ast = self.program.ast.clone();
2531 let (sketch_block_ref, _) = self
2532 .mutate_ast(
2533 &mut new_ast,
2534 sketch_id,
2535 AstMutateCommand::AddSketchBlockVarDecl {
2536 prefix: CIRCLE_VARIABLE.to_owned(),
2537 expr: circle_ast,
2538 },
2539 )
2540 .map_err(KclErrorWithOutputs::no_outputs)?;
2541 let new_source = source_from_ast(&new_ast);
2543 let new_program = parse_frontend_mutation_source(
2545 &new_source,
2546 "Error parsing KCL source after adding circle",
2547 "No AST produced after adding circle",
2548 )?;
2549
2550 let circle_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2551 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2552 "Source range of circle not found in sketch block: {sketch_block_ref:?}; {err:?}"
2553 )))
2554 })?;
2555
2556 self.program = new_program.clone();
2558
2559 let mut truncated_program = new_program;
2561 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2562 .map_err(KclErrorWithOutputs::no_outputs)?;
2563
2564 let outcome = ctx
2566 .run_mock(
2567 &truncated_program,
2568 &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2569 )
2570 .await?;
2571
2572 let new_object_ids = {
2573 let make_err =
2574 |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2575 let segment_id = outcome
2576 .source_range_to_object
2577 .get(&circle_node_ref.range)
2578 .copied()
2579 .ok_or_else(|| make_err(format!("Source range of circle not found: {circle_node_ref:?}")))?;
2580 let segment_object = outcome
2581 .scene_objects
2582 .get(segment_id.0)
2583 .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2584 let ObjectKind::Segment { segment } = &segment_object.kind else {
2585 return Err(make_err(format!(
2586 "Object is not a segment, it is {}",
2587 segment_object.kind.human_friendly_kind_with_article()
2588 )));
2589 };
2590 let Segment::Circle(circle) = segment else {
2591 return Err(make_err(format!(
2592 "Segment is not a circle, it is {}",
2593 segment.human_friendly_kind_with_article()
2594 )));
2595 };
2596 vec![circle.start, circle.center, segment_id]
2597 };
2598 let src_delta = SourceDelta { text: new_source };
2599 let outcome = self.update_state_after_exec(outcome, false);
2601 let scene_graph_delta = SceneGraphDelta {
2602 new_graph: self.scene_graph_for_ui(),
2603 invalidates_ids: false,
2604 new_objects: new_object_ids,
2605 exec_outcome: outcome,
2606 };
2607 Ok((src_delta, scene_graph_delta))
2608 }
2609
2610 async fn add_control_point_spline(
2611 &mut self,
2612 ctx: &ExecutorContext,
2613 sketch: ObjectId,
2614 ctor: ControlPointSplineCtor,
2615 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2616 let new_program = ensure_control_point_spline_experimental_features(&self.program)
2617 .map_err(KclErrorWithOutputs::no_outputs)?;
2618
2619 let points_ast = to_ast_point2d_array(&ctor.points)
2620 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2621 let mut arguments = vec![ast::LabeledArg {
2622 label: Some(ast::Identifier::new(CONTROL_POINT_SPLINE_POINTS_PARAM)),
2623 arg: points_ast,
2624 }];
2625 if ctor.construction == Some(true) {
2626 arguments.push(ast::LabeledArg {
2627 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2628 arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
2629 value: ast::LiteralValue::Bool(true),
2630 raw: "true".to_string(),
2631 digest: None,
2632 }))),
2633 });
2634 }
2635 let spline_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
2636 callee: ast::Node::no_src(ast_sketch2_name(CONTROL_POINT_SPLINE_FN)),
2637 unlabeled: None,
2638 arguments,
2639 digest: None,
2640 non_code_meta: Default::default(),
2641 })));
2642
2643 let sketch_object = self.scene_graph.objects.get(sketch.0).ok_or_else(|| {
2644 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2645 })?;
2646 let ObjectKind::Sketch(_) = &sketch_object.kind else {
2647 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2648 "Object is not a sketch, it is {}",
2649 sketch_object.kind.human_friendly_kind_with_article(),
2650 ))));
2651 };
2652
2653 let mut new_ast = new_program.ast.clone();
2654 let (sketch_block_ref, _) = self
2655 .mutate_ast(
2656 &mut new_ast,
2657 sketch,
2658 AstMutateCommand::AddSketchBlockExprStmt { expr: spline_ast },
2659 )
2660 .map_err(KclErrorWithOutputs::no_outputs)?;
2661 let new_source = source_from_ast(&new_ast);
2662 let new_program = parse_frontend_mutation_source(
2663 &new_source,
2664 "Error parsing KCL source after adding controlPointSpline",
2665 "No AST produced after adding controlPointSpline",
2666 )?;
2667
2668 let spline_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2669 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2670 "Source range of controlPointSpline not found in sketch block: {sketch_block_ref:?}; {err:?}"
2671 )))
2672 })?;
2673
2674 self.program = new_program.clone();
2675
2676 let mut truncated_program = new_program;
2677 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2678 .map_err(KclErrorWithOutputs::no_outputs)?;
2679
2680 let outcome = ctx
2681 .run_mock(
2682 &truncated_program,
2683 &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2684 )
2685 .await?;
2686
2687 let new_object_ids = {
2688 let make_err =
2689 |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2690 let segment_id = outcome
2691 .source_range_to_object
2692 .get(&spline_node_ref.range)
2693 .copied()
2694 .ok_or_else(|| {
2695 make_err(format!(
2696 "Source range of controlPointSpline not found: {spline_node_ref:?}"
2697 ))
2698 })?;
2699 let segment_object = outcome
2700 .scene_objects
2701 .get(segment_id.0)
2702 .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2703 let ObjectKind::Segment { segment } = &segment_object.kind else {
2704 return Err(make_err(format!(
2705 "Object is not a segment, it is {}",
2706 segment_object.kind.human_friendly_kind_with_article()
2707 )));
2708 };
2709 let Segment::ControlPointSpline(spline) = segment else {
2710 return Err(make_err(format!(
2711 "Segment is not a control point spline, it is {}",
2712 segment.human_friendly_kind_with_article()
2713 )));
2714 };
2715
2716 let mut ids = outcome
2717 .scene_objects
2718 .iter()
2719 .filter_map(|obj| match &obj.kind {
2720 ObjectKind::Segment {
2721 segment: Segment::Line(line),
2722 } if line.owner == Some(segment_id) => Some(obj.id),
2723 _ => None,
2724 })
2725 .collect::<Vec<_>>();
2726 ids.extend(spline.controls.clone());
2727 ids.push(segment_id);
2728 ids
2729 };
2730 let src_delta = SourceDelta { text: new_source };
2731 let outcome = self.update_state_after_exec(outcome, false);
2732 let scene_graph_delta = SceneGraphDelta {
2733 new_graph: self.scene_graph_for_ui(),
2734 invalidates_ids: false,
2735 new_objects: new_object_ids,
2736 exec_outcome: outcome,
2737 };
2738 Ok((src_delta, scene_graph_delta))
2739 }
2740
2741 fn edit_point(
2742 &mut self,
2743 new_ast: &mut ast::Node<ast::Program>,
2744 sketch: ObjectId,
2745 point: ObjectId,
2746 ctor: PointCtor,
2747 ) -> Result<(), KclError> {
2748 let new_at_ast = to_ast_point2d(&ctor.position).map_err(|err| KclError::refactor(err.to_string()))?;
2750
2751 let sketch_id = sketch;
2753 let sketch_object = self
2754 .scene_graph
2755 .objects
2756 .get(sketch_id.0)
2757 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2758 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2759 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2760 };
2761 sketch.segments.iter().find(|o| **o == point).ok_or_else(|| {
2762 KclError::refactor(format!("Point not found in sketch: point={point:?}, sketch={sketch:?}"))
2763 })?;
2764 let point_id = point;
2766 let point_object = self
2767 .scene_graph
2768 .objects
2769 .get(point_id.0)
2770 .ok_or_else(|| KclError::refactor(format!("Point not found in scene graph: point={point:?}")))?;
2771 let ObjectKind::Segment {
2772 segment: Segment::Point(point),
2773 } = &point_object.kind
2774 else {
2775 return Err(KclError::refactor(format!(
2776 "Object is not a point segment: {point_object:?}"
2777 )));
2778 };
2779
2780 if let Some(owner_id) = point.owner {
2782 let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
2783 KclError::refactor(format!(
2784 "Internal: Owner of point not found in scene graph: owner={owner_id:?}",
2785 ))
2786 })?;
2787 let ObjectKind::Segment { segment } = &owner_object.kind else {
2788 return Err(KclError::refactor(format!(
2789 "Internal: Owner of point is not a segment, but found {}",
2790 owner_object.kind.human_friendly_kind_with_article()
2791 )));
2792 };
2793
2794 if let Segment::Line(line) = segment {
2796 let SegmentCtor::Line(line_ctor) = &line.ctor else {
2797 return Err(KclError::refactor(format!(
2798 "Internal: Owner of point does not have line ctor, but found {}",
2799 line.ctor.human_friendly_kind_with_article()
2800 )));
2801 };
2802 let mut line_ctor = line_ctor.clone();
2803 if line.start == point_id {
2805 line_ctor.start = ctor.position;
2806 } else if line.end == point_id {
2807 line_ctor.end = ctor.position;
2808 } else {
2809 return Err(KclError::refactor(format!(
2810 "Internal: Point is not part of owner's line segment: point={point_id:?}, line={owner_id:?}"
2811 )));
2812 }
2813 return self.edit_line(new_ast, sketch_id, owner_id, line_ctor);
2814 }
2815
2816 if let Segment::Arc(arc) = segment {
2818 let SegmentCtor::Arc(arc_ctor) = &arc.ctor else {
2819 return Err(KclError::refactor(format!(
2820 "Internal: Owner of point does not have arc ctor, but found {}",
2821 arc.ctor.human_friendly_kind_with_article()
2822 )));
2823 };
2824 let mut arc_ctor = arc_ctor.clone();
2825 if arc.center == point_id {
2827 arc_ctor.center = ctor.position;
2828 } else if arc.start == point_id {
2829 arc_ctor.start = ctor.position;
2830 } else if arc.end == point_id {
2831 arc_ctor.end = ctor.position;
2832 } else {
2833 return Err(KclError::refactor(format!(
2834 "Internal: Point is not part of owner's arc segment: point={point_id:?}, arc={owner_id:?}"
2835 )));
2836 }
2837 return self.edit_arc(new_ast, sketch_id, owner_id, arc_ctor);
2838 }
2839
2840 if let Segment::Circle(circle) = segment {
2842 let SegmentCtor::Circle(circle_ctor) = &circle.ctor else {
2843 return Err(KclError::refactor(format!(
2844 "Internal: Owner of point does not have circle ctor, but found {}",
2845 circle.ctor.human_friendly_kind_with_article()
2846 )));
2847 };
2848 let mut circle_ctor = circle_ctor.clone();
2849 if circle.center == point_id {
2850 circle_ctor.center = ctor.position;
2851 } else if circle.start == point_id {
2852 circle_ctor.start = ctor.position;
2853 } else {
2854 return Err(KclError::refactor(format!(
2855 "Internal: Point is not part of owner's circle segment: point={point_id:?}, circle={owner_id:?}"
2856 )));
2857 }
2858 return self.edit_circle(new_ast, sketch_id, owner_id, circle_ctor);
2859 }
2860
2861 if let Segment::ControlPointSpline(spline) = segment {
2862 let SegmentCtor::ControlPointSpline(spline_ctor) = &spline.ctor else {
2863 return Err(KclError::refactor(format!(
2864 "Internal: Owner of point does not have controlPointSpline ctor, but found {}",
2865 spline.ctor.human_friendly_kind_with_article()
2866 )));
2867 };
2868 let mut spline_ctor = spline_ctor.clone();
2869 let Some(control_index) = spline.controls.iter().position(|id| *id == point_id) else {
2870 return Err(KclError::refactor(format!(
2871 "Internal: Point is not part of owner's controlPointSpline segment: point={point_id:?}, spline={owner_id:?}"
2872 )));
2873 };
2874 spline_ctor.points[control_index] = ctor.position;
2875 return self.edit_control_point_spline(new_ast, sketch_id, owner_id, spline_ctor);
2876 }
2877
2878 }
2881
2882 self.mutate_ast(new_ast, point_id, AstMutateCommand::EditPoint { at: new_at_ast })?;
2884 Ok(())
2885 }
2886
2887 fn edit_line(
2888 &mut self,
2889 new_ast: &mut ast::Node<ast::Program>,
2890 sketch: ObjectId,
2891 line: ObjectId,
2892 ctor: LineCtor,
2893 ) -> Result<(), KclError> {
2894 let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2896 let new_end_ast = to_ast_point2d(&ctor.end).map_err(|err| KclError::refactor(err.to_string()))?;
2897
2898 let sketch_id = sketch;
2900 let sketch_object = self
2901 .scene_graph
2902 .objects
2903 .get(sketch_id.0)
2904 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2905 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2906 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2907 };
2908 sketch
2909 .segments
2910 .iter()
2911 .find(|o| **o == line)
2912 .ok_or_else(|| KclError::refactor(format!("Line not found in sketch: line={line:?}, sketch={sketch:?}")))?;
2913 let line_id = line;
2915 let line_object = self
2916 .scene_graph
2917 .objects
2918 .get(line_id.0)
2919 .ok_or_else(|| KclError::refactor(format!("Line not found in scene graph: line={line:?}")))?;
2920 let ObjectKind::Segment { .. } = &line_object.kind else {
2921 let kind = line_object.kind.human_friendly_kind_with_article();
2922 return Err(KclError::refactor(format!(
2923 "This constraint only works on Segments, but you selected {kind}"
2924 )));
2925 };
2926
2927 self.mutate_ast(
2929 new_ast,
2930 line_id,
2931 AstMutateCommand::EditLine {
2932 start: new_start_ast,
2933 end: new_end_ast,
2934 construction: ctor.construction,
2935 },
2936 )?;
2937 Ok(())
2938 }
2939
2940 fn edit_arc(
2941 &mut self,
2942 new_ast: &mut ast::Node<ast::Program>,
2943 sketch: ObjectId,
2944 arc: ObjectId,
2945 ctor: ArcCtor,
2946 ) -> Result<(), KclError> {
2947 let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2949 let new_end_ast = to_ast_point2d(&ctor.end).map_err(|err| KclError::refactor(err.to_string()))?;
2950 let new_center_ast = to_ast_point2d(&ctor.center).map_err(|err| KclError::refactor(err.to_string()))?;
2951
2952 let sketch_id = sketch;
2954 let sketch_object = self
2955 .scene_graph
2956 .objects
2957 .get(sketch_id.0)
2958 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2959 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2960 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2961 };
2962 sketch
2963 .segments
2964 .iter()
2965 .find(|o| **o == arc)
2966 .ok_or_else(|| KclError::refactor(format!("Arc not found in sketch: arc={arc:?}, sketch={sketch:?}")))?;
2967 let arc_id = arc;
2969 let arc_object = self
2970 .scene_graph
2971 .objects
2972 .get(arc_id.0)
2973 .ok_or_else(|| KclError::refactor(format!("Arc not found in scene graph: arc={arc:?}")))?;
2974 let ObjectKind::Segment { .. } = &arc_object.kind else {
2975 return Err(KclError::refactor(format!("Object is not a segment: {arc_object:?}")));
2976 };
2977
2978 self.mutate_ast(
2980 new_ast,
2981 arc_id,
2982 AstMutateCommand::EditArc {
2983 start: new_start_ast,
2984 end: new_end_ast,
2985 center: new_center_ast,
2986 direction: ctor.direction,
2987 construction: ctor.construction,
2988 },
2989 )?;
2990 Ok(())
2991 }
2992
2993 fn edit_circle(
2994 &mut self,
2995 new_ast: &mut ast::Node<ast::Program>,
2996 sketch: ObjectId,
2997 circle: ObjectId,
2998 ctor: CircleCtor,
2999 ) -> Result<(), KclError> {
3000 let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
3002 let new_center_ast = to_ast_point2d(&ctor.center).map_err(|err| KclError::refactor(err.to_string()))?;
3003
3004 let sketch_id = sketch;
3006 let sketch_object = self
3007 .scene_graph
3008 .objects
3009 .get(sketch_id.0)
3010 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
3011 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
3012 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
3013 };
3014 sketch.segments.iter().find(|o| **o == circle).ok_or_else(|| {
3015 KclError::refactor(format!(
3016 "Circle not found in sketch: circle={circle:?}, sketch={sketch:?}"
3017 ))
3018 })?;
3019 let circle_id = circle;
3021 let circle_object = self
3022 .scene_graph
3023 .objects
3024 .get(circle_id.0)
3025 .ok_or_else(|| KclError::refactor(format!("Circle not found in scene graph: circle={circle:?}")))?;
3026 let ObjectKind::Segment { .. } = &circle_object.kind else {
3027 return Err(KclError::refactor(format!(
3028 "Object is not a segment: {circle_object:?}"
3029 )));
3030 };
3031
3032 self.mutate_ast(
3034 new_ast,
3035 circle_id,
3036 AstMutateCommand::EditCircle {
3037 start: new_start_ast,
3038 center: new_center_ast,
3039 construction: ctor.construction,
3040 },
3041 )?;
3042 Ok(())
3043 }
3044
3045 fn edit_control_point_spline(
3046 &mut self,
3047 new_ast: &mut ast::Node<ast::Program>,
3048 sketch: ObjectId,
3049 spline: ObjectId,
3050 ctor: ControlPointSplineCtor,
3051 ) -> Result<(), KclError> {
3052 let points_ast = to_ast_point2d_array(&ctor.points).map_err(|err| KclError::refactor(err.to_string()))?;
3053
3054 let sketch_object = self
3055 .scene_graph
3056 .objects
3057 .get(sketch.0)
3058 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
3059 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
3060 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
3061 };
3062 sketch.segments.iter().find(|o| **o == spline).ok_or_else(|| {
3063 KclError::refactor(format!(
3064 "Control point spline not found in sketch: spline={spline:?}, sketch={sketch:?}"
3065 ))
3066 })?;
3067
3068 let spline_object =
3069 self.scene_graph.objects.get(spline.0).ok_or_else(|| {
3070 KclError::refactor(format!("Control point spline not found in scene graph: {spline:?}"))
3071 })?;
3072 let ObjectKind::Segment { .. } = &spline_object.kind else {
3073 return Err(KclError::refactor(format!(
3074 "Object is not a segment: {spline_object:?}"
3075 )));
3076 };
3077
3078 self.mutate_ast(
3079 new_ast,
3080 spline,
3081 AstMutateCommand::EditControlPointSpline {
3082 points: points_ast,
3083 construction: ctor.construction,
3084 },
3085 )?;
3086 Ok(())
3087 }
3088
3089 fn delete_segment(
3090 &mut self,
3091 new_ast: &mut ast::Node<ast::Program>,
3092 sketch: ObjectId,
3093 segment_id: ObjectId,
3094 ) -> Result<(), KclError> {
3095 let sketch_id = sketch;
3097 let sketch_object = self
3098 .scene_graph
3099 .objects
3100 .get(sketch_id.0)
3101 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
3102 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
3103 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
3104 };
3105 sketch.segments.iter().find(|o| **o == segment_id).ok_or_else(|| {
3106 KclError::refactor(format!(
3107 "Segment not found in sketch: segment={segment_id:?}, sketch={sketch:?}"
3108 ))
3109 })?;
3110 let segment_object =
3112 self.scene_graph.objects.get(segment_id.0).ok_or_else(|| {
3113 KclError::refactor(format!("Segment not found in scene graph: segment={segment_id:?}"))
3114 })?;
3115 let ObjectKind::Segment { .. } = &segment_object.kind else {
3116 return Err(KclError::refactor(format!(
3117 "Object is not a segment, it is {}",
3118 segment_object.kind.human_friendly_kind_with_article()
3119 )));
3120 };
3121
3122 self.mutate_ast(new_ast, segment_id, AstMutateCommand::DeleteNode)?;
3124 Ok(())
3125 }
3126
3127 fn delete_constraint(
3128 &mut self,
3129 new_ast: &mut ast::Node<ast::Program>,
3130 sketch: ObjectId,
3131 constraint_id: ObjectId,
3132 ) -> Result<(), KclError> {
3133 let sketch_id = sketch;
3135 let sketch_object = self
3136 .scene_graph
3137 .objects
3138 .get(sketch_id.0)
3139 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
3140 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
3141 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
3142 };
3143 sketch
3144 .constraints
3145 .iter()
3146 .find(|o| **o == constraint_id)
3147 .ok_or_else(|| {
3148 KclError::refactor(format!(
3149 "Constraint not found in sketch: constraint={constraint_id:?}, sketch={sketch:?}"
3150 ))
3151 })?;
3152 let constraint_object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
3154 KclError::refactor(format!(
3155 "Constraint not found in scene graph: constraint={constraint_id:?}"
3156 ))
3157 })?;
3158 let ObjectKind::Constraint { .. } = &constraint_object.kind else {
3159 return Err(KclError::refactor(format!(
3160 "Object is not a constraint, it is {}",
3161 constraint_object.kind.human_friendly_kind_with_article()
3162 )));
3163 };
3164
3165 self.mutate_ast(new_ast, constraint_id, AstMutateCommand::DeleteNode)?;
3167 Ok(())
3168 }
3169
3170 fn edit_coincident_constraint(
3171 &mut self,
3172 new_ast: &mut ast::Node<ast::Program>,
3173 constraint_id: ObjectId,
3174 segments: Vec<ConstraintSegment>,
3175 ) -> Result<(), KclError> {
3176 if segments.len() < 2 {
3177 return Err(KclError::refactor(format!(
3178 "Coincident constraint must have at least 2 inputs, got {}",
3179 segments.len()
3180 )));
3181 }
3182
3183 let segment_asts = segments
3184 .iter()
3185 .map(|segment| self.coincident_segment_to_ast(segment, new_ast))
3186 .collect::<Result<Vec<_>, _>>()?;
3187
3188 let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
3189 elements: segment_asts,
3190 digest: None,
3191 non_code_meta: Default::default(),
3192 })));
3193
3194 self.mutate_ast(
3195 new_ast,
3196 constraint_id,
3197 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3198 )?;
3199 Ok(())
3200 }
3201
3202 fn edit_horizontal_points_constraint(
3203 &mut self,
3204 new_ast: &mut ast::Node<ast::Program>,
3205 constraint_id: ObjectId,
3206 points: Vec<ConstraintSegment>,
3207 ) -> Result<(), KclError> {
3208 self.edit_axis_points_constraint(new_ast, constraint_id, points, "Horizontal")
3209 }
3210
3211 fn edit_vertical_points_constraint(
3212 &mut self,
3213 new_ast: &mut ast::Node<ast::Program>,
3214 constraint_id: ObjectId,
3215 points: Vec<ConstraintSegment>,
3216 ) -> Result<(), KclError> {
3217 self.edit_axis_points_constraint(new_ast, constraint_id, points, "Vertical")
3218 }
3219
3220 fn edit_axis_points_constraint(
3221 &mut self,
3222 new_ast: &mut ast::Node<ast::Program>,
3223 constraint_id: ObjectId,
3224 points: Vec<ConstraintSegment>,
3225 constraint_name: &str,
3226 ) -> Result<(), KclError> {
3227 if points.len() < 2 {
3228 return Err(KclError::refactor(format!(
3229 "{constraint_name} points constraint must have at least 2 points, got {}",
3230 points.len()
3231 )));
3232 }
3233
3234 let point_asts = points
3235 .iter()
3236 .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
3237 .collect::<Result<Vec<_>, _>>()?;
3238
3239 let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
3240 elements: point_asts,
3241 digest: None,
3242 non_code_meta: Default::default(),
3243 })));
3244
3245 self.mutate_ast(
3246 new_ast,
3247 constraint_id,
3248 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3249 )?;
3250 Ok(())
3251 }
3252
3253 fn edit_equal_length_constraint(
3255 &mut self,
3256 new_ast: &mut ast::Node<ast::Program>,
3257 constraint_id: ObjectId,
3258 lines: Vec<ObjectId>,
3259 ) -> Result<(), KclError> {
3260 if lines.len() < 2 {
3261 return Err(KclError::refactor(format!(
3262 "Lines equal length constraint must have at least 2 lines, got {}",
3263 lines.len()
3264 )));
3265 }
3266
3267 let line_asts = lines
3268 .iter()
3269 .map(|line_id| {
3270 let line_object = self
3271 .scene_graph
3272 .objects
3273 .get(line_id.0)
3274 .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3275 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3276 let kind = line_object.kind.human_friendly_kind_with_article();
3277 return Err(KclError::refactor(format!(
3278 "This constraint only works on Segments, but you selected {kind}"
3279 )));
3280 };
3281 let Segment::Line(_) = line_segment else {
3282 let kind = line_segment.human_friendly_kind_with_article();
3283 return Err(KclError::refactor(format!(
3284 "Only lines can be made equal length, but you selected {kind}"
3285 )));
3286 };
3287
3288 get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)
3289 })
3290 .collect::<Result<Vec<_>, _>>()?;
3291
3292 let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
3293 elements: line_asts,
3294 digest: None,
3295 non_code_meta: Default::default(),
3296 })));
3297
3298 self.mutate_ast(
3299 new_ast,
3300 constraint_id,
3301 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3302 )?;
3303 Ok(())
3304 }
3305
3306 fn edit_parallel_constraint(
3308 &mut self,
3309 new_ast: &mut ast::Node<ast::Program>,
3310 constraint_id: ObjectId,
3311 lines: Vec<ObjectId>,
3312 ) -> Result<(), KclError> {
3313 if lines.len() < 2 {
3314 return Err(KclError::refactor(format!(
3315 "Parallel constraint must have at least 2 lines, got {}",
3316 lines.len()
3317 )));
3318 }
3319
3320 let line_asts = lines
3321 .iter()
3322 .map(|line_id| {
3323 let line_object = self
3324 .scene_graph
3325 .objects
3326 .get(line_id.0)
3327 .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3328 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3329 let kind = line_object.kind.human_friendly_kind_with_article();
3330 return Err(KclError::refactor(format!(
3331 "This constraint only works on Segments, but you selected {kind}"
3332 )));
3333 };
3334 let Segment::Line(_) = line_segment else {
3335 let kind = line_segment.human_friendly_kind_with_article();
3336 return Err(KclError::refactor(format!(
3337 "Only lines can be made parallel, but you selected {kind}"
3338 )));
3339 };
3340
3341 get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)
3342 })
3343 .collect::<Result<Vec<_>, _>>()?;
3344
3345 let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
3346 elements: line_asts,
3347 digest: None,
3348 non_code_meta: Default::default(),
3349 })));
3350
3351 self.mutate_ast(
3352 new_ast,
3353 constraint_id,
3354 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3355 )?;
3356 Ok(())
3357 }
3358
3359 fn edit_equal_radius_constraint(
3361 &mut self,
3362 new_ast: &mut ast::Node<ast::Program>,
3363 constraint_id: ObjectId,
3364 input: Vec<ObjectId>,
3365 ) -> Result<(), KclError> {
3366 if input.len() < 2 {
3367 return Err(KclError::refactor(format!(
3368 "equalRadius constraint must have at least 2 segments, got {}",
3369 input.len()
3370 )));
3371 }
3372
3373 let input_asts = input
3374 .iter()
3375 .map(|segment_id| self.equal_radius_segment_id_to_ast_reference(*segment_id, new_ast))
3376 .collect::<Result<Vec<_>, _>>()?;
3377
3378 let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
3379 elements: input_asts,
3380 digest: None,
3381 non_code_meta: Default::default(),
3382 })));
3383
3384 self.mutate_ast(
3385 new_ast,
3386 constraint_id,
3387 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3388 )?;
3389 Ok(())
3390 }
3391
3392 async fn execute_after_edit(
3393 &mut self,
3394 ctx: &ExecutorContext,
3395 sketch: ObjectId,
3396 sketch_block_ref: AstNodeRef,
3397 new_ast: &mut ast::Node<ast::Program>,
3398 options: ExecuteAfterEditOptions,
3399 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
3400 let ExecuteAfterEditOptions {
3401 segment_ids_edited,
3402 edit_kind,
3403 commit_solved_initial_guesses,
3404 } = options;
3405
3406 let new_source = source_from_ast(new_ast);
3408 let new_program = parse_frontend_mutation_source(
3410 &new_source,
3411 "Error parsing KCL source after editing",
3412 "No AST produced after editing",
3413 )?;
3414
3415 let is_delete = edit_kind.is_delete();
3417 let truncated_program = {
3418 let mut truncated_program = new_program.clone();
3419 only_sketch_block(
3420 &mut truncated_program.ast,
3421 &sketch_block_ref,
3422 edit_kind.to_change_kind(),
3423 )
3424 .map_err(KclErrorWithOutputs::no_outputs)?;
3425 truncated_program
3426 };
3427
3428 let drag_anchors = self.next_segment_drag_anchors.take().unwrap_or_default();
3430 let mock_config = MockConfig {
3431 sketch_block_id: Some(sketch),
3432 freedom_analysis: is_delete,
3433 segment_ids_edited: segment_ids_edited.clone(),
3434 drag_anchors,
3435 ..Default::default()
3436 };
3437 let outcome = ctx.run_mock(&truncated_program, &mock_config).await?;
3438
3439 self.program = new_program;
3441
3442 let outcome = self.update_state_after_exec(outcome, is_delete);
3444
3445 let src_delta = if commit_solved_initial_guesses {
3446 self.commit_var_solutions_to_program(&outcome, "editing")?
3447 } else {
3448 SourceDelta { text: new_source }
3449 };
3450 let scene_graph_delta = SceneGraphDelta {
3451 new_graph: self.scene_graph_for_ui(),
3452 invalidates_ids: is_delete,
3453 new_objects: Vec::new(),
3454 exec_outcome: outcome,
3455 };
3456 Ok((src_delta, scene_graph_delta))
3457 }
3458
3459 async fn execute_after_delete_sketch(
3460 &mut self,
3461 ctx: &ExecutorContext,
3462 new_ast: &mut ast::Node<ast::Program>,
3463 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
3464 let new_source = source_from_ast(new_ast);
3466 let new_program = parse_frontend_mutation_source(
3468 &new_source,
3469 "Error parsing KCL source after editing",
3470 "No AST produced after editing",
3471 )?;
3472
3473 self.program = new_program.clone();
3475
3476 let outcome = ctx.run_with_caching(new_program).await?;
3482 let freedom_analysis_ran = true;
3483
3484 let outcome = self.update_state_after_exec(outcome, freedom_analysis_ran);
3485
3486 let src_delta = SourceDelta { text: new_source };
3487 let scene_graph_delta = SceneGraphDelta {
3488 new_graph: self.scene_graph_for_ui(),
3489 invalidates_ids: true,
3490 new_objects: Vec::new(),
3491 exec_outcome: outcome,
3492 };
3493 Ok((src_delta, scene_graph_delta))
3494 }
3495
3496 fn point_id_to_ast_reference(
3501 &self,
3502 point_id: ObjectId,
3503 new_ast: &mut ast::Node<ast::Program>,
3504 ) -> Result<ast::Expr, KclError> {
3505 let point_object = self
3506 .scene_graph
3507 .objects
3508 .get(point_id.0)
3509 .ok_or_else(|| KclError::refactor(format!("Point not found: {point_id:?}")))?;
3510 let ObjectKind::Segment { segment: point_segment } = &point_object.kind else {
3511 return Err(KclError::refactor(format!("Object is not a segment: {point_object:?}")));
3512 };
3513 let Segment::Point(point) = point_segment else {
3514 return Err(KclError::refactor(format!(
3515 "Only points are currently supported: {point_object:?}"
3516 )));
3517 };
3518
3519 if let Some(owner_id) = point.owner {
3520 let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
3521 KclError::refactor(format!(
3522 "Owner of point not found in scene graph: point={point_id:?}, owner={owner_id:?}"
3523 ))
3524 })?;
3525 let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind else {
3526 return Err(KclError::refactor(format!(
3527 "Owner of point is not a segment, but found {}",
3528 owner_object.kind.human_friendly_kind_with_article()
3529 )));
3530 };
3531
3532 match owner_segment {
3533 Segment::Line(line) => {
3534 let property = if line.start == point_id {
3535 LINE_PROPERTY_START
3536 } else if line.end == point_id {
3537 LINE_PROPERTY_END
3538 } else {
3539 return Err(KclError::refactor(format!(
3540 "Internal: Point is not part of owner's line segment: point={point_id:?}, line={owner_id:?}"
3541 )));
3542 };
3543 get_or_insert_ast_reference(new_ast, &owner_object.source, LINE_VARIABLE, Some(property))
3544 }
3545 Segment::Arc(arc) => {
3546 let property = if arc.start == point_id {
3547 ARC_PROPERTY_START
3548 } else if arc.end == point_id {
3549 ARC_PROPERTY_END
3550 } else if arc.center == point_id {
3551 ARC_PROPERTY_CENTER
3552 } else {
3553 return Err(KclError::refactor(format!(
3554 "Internal: Point is not part of owner's arc segment: point={point_id:?}, arc={owner_id:?}"
3555 )));
3556 };
3557 get_or_insert_ast_reference(new_ast, &owner_object.source, ARC_VARIABLE, Some(property))
3558 }
3559 Segment::Circle(circle) => {
3560 let property = if circle.start == point_id {
3561 CIRCLE_PROPERTY_START
3562 } else if circle.center == point_id {
3563 CIRCLE_PROPERTY_CENTER
3564 } else {
3565 return Err(KclError::refactor(format!(
3566 "Internal: Point is not part of owner's circle segment: point={point_id:?}, circle={owner_id:?}"
3567 )));
3568 };
3569 get_or_insert_ast_reference(new_ast, &owner_object.source, CIRCLE_VARIABLE, Some(property))
3570 }
3571 Segment::ControlPointSpline(spline) => {
3572 let Some(index) = spline.controls.iter().position(|id| *id == point_id) else {
3573 return Err(KclError::refactor(format!(
3574 "Internal: Point is not part of owner's controlPointSpline segment: point={point_id:?}, spline={owner_id:?}"
3575 )));
3576 };
3577 let owner_expr =
3578 get_or_insert_ast_reference(new_ast, &owner_object.source, CONTROL_POINT_SPLINE_FN, None)?;
3579 let controls_expr = create_member_expression(owner_expr, CONTROL_POINT_SPLINE_PROPERTY_CONTROLS);
3580 Ok(create_index_expression(controls_expr, index))
3581 }
3582 _ => Err(KclError::refactor(format!(
3583 "Internal: Owner of point is not a supported segment type for constraints: {owner_segment:?}"
3584 ))),
3585 }
3586 } else {
3587 get_or_insert_ast_reference(new_ast, &point_object.source, "point", None)
3589 }
3590 }
3591
3592 fn line_id_to_ast_reference(
3593 &self,
3594 line_id: ObjectId,
3595 new_ast: &mut ast::Node<ast::Program>,
3596 ) -> Result<ast::Expr, KclError> {
3597 let line_object = self
3598 .scene_graph
3599 .objects
3600 .get(line_id.0)
3601 .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3602 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3603 return Err(KclError::refactor(format!("Object is not a segment: {line_object:?}")));
3604 };
3605 let Segment::Line(line) = line_segment else {
3606 return Err(KclError::refactor(format!(
3607 "Only lines are currently supported: {line_object:?}"
3608 )));
3609 };
3610
3611 if let Some(owner_id) = line.owner {
3612 let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
3613 KclError::refactor(format!(
3614 "Owner of line not found in scene graph: line={line_id:?}, owner={owner_id:?}"
3615 ))
3616 })?;
3617 let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind else {
3618 return Err(KclError::refactor(format!(
3619 "Owner of line is not a segment, but found {}",
3620 owner_object.kind.human_friendly_kind_with_article()
3621 )));
3622 };
3623
3624 match owner_segment {
3625 Segment::ControlPointSpline(spline) => {
3626 let Some(index) = spline
3627 .controls
3628 .windows(2)
3629 .position(|window| window[0] == line.start && window[1] == line.end)
3630 else {
3631 return Err(KclError::refactor(format!(
3632 "Internal: Line is not part of owner's controlPointSpline segment: line={line_id:?}, spline={owner_id:?}"
3633 )));
3634 };
3635 let owner_expr =
3636 get_or_insert_ast_reference(new_ast, &owner_object.source, CONTROL_POINT_SPLINE_FN, None)?;
3637 let edges_expr = create_member_expression(owner_expr, CONTROL_POINT_SPLINE_PROPERTY_EDGES);
3638 Ok(create_index_expression(edges_expr, index))
3639 }
3640 _ => Err(KclError::refactor(format!(
3641 "Internal: Owner of line is not a supported segment type for constraints: {owner_segment:?}"
3642 ))),
3643 }
3644 } else {
3645 get_or_insert_ast_reference(new_ast, &line_object.source, "line", None)
3646 }
3647 }
3648
3649 fn coincident_segment_to_ast(
3650 &self,
3651 segment: &ConstraintSegment,
3652 new_ast: &mut ast::Node<ast::Program>,
3653 ) -> Result<ast::Expr, KclError> {
3654 match segment {
3655 ConstraintSegment::Origin(_) => Ok(ast_name_expr("ORIGIN".to_owned())),
3656 ConstraintSegment::Segment(segment_id) => self.segment_id_to_constraint_ast_reference(*segment_id, new_ast),
3657 }
3658 }
3659
3660 fn segment_id_to_constraint_ast_reference(
3661 &self,
3662 segment_id: ObjectId,
3663 new_ast: &mut ast::Node<ast::Program>,
3664 ) -> Result<ast::Expr, KclError> {
3665 let segment_object = self
3666 .scene_graph
3667 .objects
3668 .get(segment_id.0)
3669 .ok_or_else(|| KclError::refactor(format!("Object not found: {segment_id:?}")))?;
3670 let ObjectKind::Segment { segment } = &segment_object.kind else {
3671 return Err(KclError::refactor(format!(
3672 "Object is not a segment, it is {}",
3673 segment_object.kind.human_friendly_kind_with_article()
3674 )));
3675 };
3676
3677 match segment {
3678 Segment::Point(_) => self.point_id_to_ast_reference(segment_id, new_ast),
3679 Segment::Line(_) => self.line_id_to_ast_reference(segment_id, new_ast),
3680 Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, "arc", None),
3681 Segment::Circle(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, CIRCLE_VARIABLE, None),
3682 Segment::ControlPointSpline(_) => {
3683 get_or_insert_ast_reference(new_ast, &segment_object.source, CONTROL_POINT_SPLINE_FN, None)
3684 }
3685 }
3686 }
3687
3688 fn axis_constraint_segment_to_ast(
3689 &self,
3690 segment: &ConstraintSegment,
3691 new_ast: &mut ast::Node<ast::Program>,
3692 ) -> Result<ast::Expr, KclError> {
3693 match segment {
3694 ConstraintSegment::Origin(_) => Ok(ast_name_expr("ORIGIN".to_owned())),
3695 ConstraintSegment::Segment(point_id) => self.point_id_to_ast_reference(*point_id, new_ast),
3696 }
3697 }
3698
3699 async fn add_coincident(
3700 &mut self,
3701 sketch: ObjectId,
3702 coincident: Coincident,
3703 new_ast: &mut ast::Node<ast::Program>,
3704 ) -> Result<AstNodeRef, KclError> {
3705 let sketch_id = sketch;
3706 for segment in &coincident.segments {
3707 let ConstraintSegment::Segment(segment_id) = segment else {
3708 continue;
3709 };
3710 let Some(segment_object) = self.scene_graph.objects.get(segment_id.0) else {
3711 continue;
3712 };
3713 if matches!(
3714 segment_object.kind,
3715 ObjectKind::Segment {
3716 segment: Segment::ControlPointSpline(_)
3717 }
3718 ) {
3719 return Err(KclError::refactor(
3720 "Coincident with a full controlPointSpline is not supported yet. Constrain a control point or spline edge instead."
3721 .to_owned(),
3722 ));
3723 }
3724 }
3725 let segment_asts = coincident
3726 .segments
3727 .iter()
3728 .map(|segment| self.coincident_segment_to_ast(segment, new_ast))
3729 .collect::<Result<Vec<_>, _>>()?;
3730 if segment_asts.len() < 2 {
3731 return Err(KclError::refactor(format!(
3732 "Coincident constraint must have at least 2 inputs, got {}",
3733 segment_asts.len()
3734 )));
3735 }
3736
3737 let coincident_ast = create_coincident_ast(segment_asts);
3739
3740 let (sketch_block_ref, _) = self.mutate_ast(
3742 new_ast,
3743 sketch_id,
3744 AstMutateCommand::AddSketchBlockExprStmt { expr: coincident_ast },
3745 )?;
3746 Ok(sketch_block_ref)
3747 }
3748
3749 async fn add_distance(
3750 &mut self,
3751 sketch: ObjectId,
3752 distance: Distance,
3753 new_ast: &mut ast::Node<ast::Program>,
3754 ) -> Result<AstNodeRef, KclError> {
3755 self.add_distance_constraint(sketch, DISTANCE_FN, distance, new_ast)
3756 }
3757
3758 fn distance_constraint_ast_parts(
3759 &self,
3760 function_name: &str,
3761 distance: &Distance,
3762 new_ast: &mut ast::Node<ast::Program>,
3763 ) -> Result<(ast::BinaryPart, ast::BinaryPart), KclError> {
3764 let [segment0_ast, segment1_ast] = match distance.segments.as_slice() {
3765 [pt0, pt1] => [
3766 self.coincident_segment_to_ast(pt0, new_ast)?,
3767 self.coincident_segment_to_ast(pt1, new_ast)?,
3768 ],
3769 _ => {
3770 return Err(KclError::refactor(format!(
3771 "Distance constraint must have exactly 2 segments, got {}",
3772 distance.segments.len()
3773 )));
3774 }
3775 };
3776
3777 let arguments = match &distance.label_position {
3778 Some(label_position) => vec![ast::LabeledArg {
3779 label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
3780 arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
3781 }],
3782 None => Default::default(),
3783 };
3784
3785 let call = ast::BinaryPart::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
3786 callee: ast::Node::no_src(ast_sketch2_name(function_name)),
3787 unlabeled: Some(ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(
3788 ast::ArrayExpression {
3789 elements: vec![segment0_ast, segment1_ast],
3790 digest: None,
3791 non_code_meta: Default::default(),
3792 },
3793 )))),
3794 arguments,
3795 digest: None,
3796 non_code_meta: Default::default(),
3797 })));
3798 let value = ast::BinaryPart::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
3799 value: ast::LiteralValue::Number {
3800 value: distance.distance.value,
3801 suffix: distance.distance.units,
3802 },
3803 raw: format_number_literal(distance.distance.value, distance.distance.units, None).map_err(|_| {
3804 KclError::refactor(format!(
3805 "Could not format numeric suffix: {:?}",
3806 distance.distance.units
3807 ))
3808 })?,
3809 digest: None,
3810 })));
3811
3812 Ok((call, value))
3813 }
3814
3815 fn add_distance_constraint(
3816 &mut self,
3817 sketch: ObjectId,
3818 function_name: &str,
3819 distance: Distance,
3820 new_ast: &mut ast::Node<ast::Program>,
3821 ) -> Result<AstNodeRef, KclError> {
3822 let (call, value) = self.distance_constraint_ast_parts(function_name, &distance, new_ast)?;
3823 let distance_ast = ast::Expr::BinaryExpression(BoxNode::new(ast::Node::no_src(ast::BinaryExpression {
3824 left: call,
3825 operator: ast::BinaryOperator::Eq,
3826 right: value,
3827 digest: None,
3828 })));
3829
3830 let (sketch_block_ref, _) = self.mutate_ast(
3831 new_ast,
3832 sketch,
3833 AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
3834 )?;
3835 Ok(sketch_block_ref)
3836 }
3837
3838 async fn add_angle(
3839 &mut self,
3840 sketch: ObjectId,
3841 angle: Angle,
3842 new_ast: &mut ast::Node<ast::Program>,
3843 ) -> Result<AstNodeRef, KclError> {
3844 let sketch_id = sketch;
3845 let (angle_call_ast, angle_value_ast) = self.angle_constraint_ast_parts(&angle, new_ast)?;
3846 let angle_ast = ast::Expr::BinaryExpression(BoxNode::new(ast::Node::no_src(ast::BinaryExpression {
3847 left: angle_call_ast,
3848 operator: ast::BinaryOperator::Eq,
3849 right: angle_value_ast,
3850 digest: None,
3851 })));
3852
3853 let (sketch_block_ref, _) = self.mutate_ast(
3855 new_ast,
3856 sketch_id,
3857 AstMutateCommand::AddSketchBlockExprStmt { expr: angle_ast },
3858 )?;
3859 Ok(sketch_block_ref)
3860 }
3861
3862 fn angle_constraint_ast_parts(
3863 &self,
3864 angle: &Angle,
3865 new_ast: &mut ast::Node<ast::Program>,
3866 ) -> Result<(ast::BinaryPart, ast::BinaryPart), KclError> {
3867 let &[l0_id, l1_id] = angle.lines.as_slice() else {
3868 return Err(KclError::refactor(format!(
3869 "Angle constraint must have exactly 2 lines, got {}",
3870 angle.lines.len()
3871 )));
3872 };
3873
3874 let l0_ast = self.line_id_to_ast_reference(l0_id, new_ast)?;
3875 let l1_ast = self.line_id_to_ast_reference(l1_id, new_ast)?;
3876 let lines_ast = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
3877 elements: vec![l0_ast, l1_ast],
3878 digest: None,
3879 non_code_meta: Default::default(),
3880 })));
3881
3882 if angle.inverse == Some(true) && angle.sector.is_none() {
3883 return Err(KclError::refactor("Angle inverse requires an angle sector".to_owned()));
3884 }
3885
3886 let uses_angle_dimension = angle.sector.is_some();
3887 let mut arguments = if uses_angle_dimension {
3888 vec![ast::LabeledArg {
3889 label: Some(ast::Identifier::new(ANGLE_LINES_PARAM)),
3890 arg: lines_ast.clone(),
3891 }]
3892 } else {
3893 Default::default()
3894 };
3895
3896 if let Some(sector) = angle.sector {
3897 arguments.push(ast::LabeledArg {
3898 label: Some(ast::Identifier::new(ANGLE_SECTOR_PARAM)),
3899 arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
3900 value: ast::LiteralValue::Number {
3901 value: f64::from(sector),
3902 suffix: NumericSuffix::None,
3903 },
3904 raw: sector.to_string(),
3905 digest: None,
3906 }))),
3907 });
3908 }
3909
3910 if angle.inverse == Some(true) {
3911 arguments.push(ast::LabeledArg {
3912 label: Some(ast::Identifier::new(ANGLE_INVERSE_PARAM)),
3913 arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
3914 value: ast::LiteralValue::Bool(true),
3915 raw: true.to_string(),
3916 digest: None,
3917 }))),
3918 });
3919 }
3920
3921 if let Some(label_position) = &angle.label_position {
3922 arguments.push(ast::LabeledArg {
3923 label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
3924 arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
3925 });
3926 }
3927
3928 let call = ast::BinaryPart::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
3929 callee: ast::Node::no_src(ast_sketch2_name(if uses_angle_dimension {
3930 ANGLE_DIMENSION_FN
3931 } else {
3932 ANGLE_FN
3933 })),
3934 unlabeled: (!uses_angle_dimension).then_some(lines_ast),
3935 arguments,
3936 digest: None,
3937 non_code_meta: Default::default(),
3938 })));
3939 let value = ast::BinaryPart::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
3940 value: ast::LiteralValue::Number {
3941 value: angle.angle.value,
3942 suffix: angle.angle.units,
3943 },
3944 raw: format_number_literal(angle.angle.value, angle.angle.units, None)
3945 .map_err(|_| KclError::refactor(format!("Could not format numeric suffix: {:?}", angle.angle.units)))?,
3946 digest: None,
3947 })));
3948
3949 Ok((call, value))
3950 }
3951
3952 async fn add_tangent(
3953 &mut self,
3954 sketch: ObjectId,
3955 tangent: Tangent,
3956 new_ast: &mut ast::Node<ast::Program>,
3957 ) -> Result<AstNodeRef, KclError> {
3958 let &[seg0_id, seg1_id] = tangent.input.as_slice() else {
3959 return Err(KclError::refactor(format!(
3960 "Tangent constraint must have exactly 2 segments, got {}",
3961 tangent.input.len()
3962 )));
3963 };
3964 let sketch_id = sketch;
3965
3966 let seg0_object = self
3967 .scene_graph
3968 .objects
3969 .get(seg0_id.0)
3970 .ok_or_else(|| KclError::refactor(format!("Segment not found: {seg0_id:?}")))?;
3971 let ObjectKind::Segment { segment: seg0_segment } = &seg0_object.kind else {
3972 return Err(KclError::refactor(format!("Object is not a segment: {seg0_object:?}")));
3973 };
3974 let seg0_ast = match seg0_segment {
3975 Segment::Line(_) | Segment::Arc(_) | Segment::Circle(_) => {
3976 self.segment_id_to_constraint_ast_reference(seg0_id, new_ast)?
3977 }
3978 _ => {
3979 return Err(KclError::refactor(format!(
3980 "Tangent supports only line/arc/circle segments for now, got: {seg0_segment:?}"
3981 )));
3982 }
3983 };
3984
3985 let seg1_object = self
3986 .scene_graph
3987 .objects
3988 .get(seg1_id.0)
3989 .ok_or_else(|| KclError::refactor(format!("Segment not found: {seg1_id:?}")))?;
3990 let ObjectKind::Segment { segment: seg1_segment } = &seg1_object.kind else {
3991 return Err(KclError::refactor(format!("Object is not a segment: {seg1_object:?}")));
3992 };
3993 let seg1_ast = match seg1_segment {
3994 Segment::Line(_) | Segment::Arc(_) | Segment::Circle(_) => {
3995 self.segment_id_to_constraint_ast_reference(seg1_id, new_ast)?
3996 }
3997 _ => {
3998 return Err(KclError::refactor(format!(
3999 "Tangent supports only line/arc/circle segments for now, got: {seg1_segment:?}"
4000 )));
4001 }
4002 };
4003
4004 let tangent_ast = create_tangent_ast(seg0_ast, seg1_ast);
4005 let (sketch_block_ref, _) = self.mutate_ast(
4006 new_ast,
4007 sketch_id,
4008 AstMutateCommand::AddSketchBlockExprStmt { expr: tangent_ast },
4009 )?;
4010 Ok(sketch_block_ref)
4011 }
4012
4013 async fn add_symmetric(
4014 &mut self,
4015 sketch: ObjectId,
4016 symmetric: Symmetric,
4017 new_ast: &mut ast::Node<ast::Program>,
4018 ) -> Result<AstNodeRef, KclError> {
4019 let &[input0_id, input1_id] = symmetric.input.as_slice() else {
4020 return Err(KclError::refactor(format!(
4021 "Symmetric constraint must have exactly 2 inputs, got {}",
4022 symmetric.input.len()
4023 )));
4024 };
4025 let sketch_id = sketch;
4026
4027 let input0_ast = self.symmetric_input_id_to_ast_reference(input0_id, new_ast)?;
4028 let input1_ast = self.symmetric_input_id_to_ast_reference(input1_id, new_ast)?;
4029 let axis_ast = self.symmetric_axis_id_to_ast_reference(symmetric.axis, new_ast)?;
4030
4031 let symmetric_ast = create_symmetric_ast(vec![input0_ast, input1_ast], axis_ast);
4032 let (sketch_block_ref, _) = self.mutate_ast(
4033 new_ast,
4034 sketch_id,
4035 AstMutateCommand::AddSketchBlockExprStmt { expr: symmetric_ast },
4036 )?;
4037 Ok(sketch_block_ref)
4038 }
4039
4040 async fn add_midpoint(
4041 &mut self,
4042 sketch: ObjectId,
4043 midpoint: Midpoint,
4044 new_ast: &mut ast::Node<ast::Program>,
4045 ) -> Result<AstNodeRef, KclError> {
4046 let sketch_id = sketch;
4047 let point_ast = self.axis_constraint_segment_to_ast(&midpoint.point, new_ast)?;
4048
4049 let segment_object = self
4050 .scene_graph
4051 .objects
4052 .get(midpoint.segment.0)
4053 .ok_or_else(|| KclError::refactor(format!("Segment not found: {:?}", midpoint.segment)))?;
4054 let ObjectKind::Segment {
4055 segment: midpoint_segment,
4056 } = &segment_object.kind
4057 else {
4058 return Err(KclError::refactor(format!(
4059 "Object must be a segment, but it was {}",
4060 segment_object.kind.human_friendly_kind_with_article()
4061 )));
4062 };
4063 let segment_ast = match midpoint_segment {
4064 Segment::Line(_) => self.line_id_to_ast_reference(midpoint.segment, new_ast)?,
4065 Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, "arc", None)?,
4066 _ => {
4067 return Err(KclError::refactor(format!(
4068 "Midpoint target must be a line or arc segment but it was {}",
4069 midpoint_segment.human_friendly_kind_with_article()
4070 )));
4071 }
4072 };
4073
4074 let midpoint_ast = create_midpoint_ast(segment_ast, point_ast);
4075 let (sketch_block_ref, _) = self.mutate_ast(
4076 new_ast,
4077 sketch_id,
4078 AstMutateCommand::AddSketchBlockExprStmt { expr: midpoint_ast },
4079 )?;
4080 Ok(sketch_block_ref)
4081 }
4082
4083 async fn add_equal_radius(
4084 &mut self,
4085 sketch: ObjectId,
4086 equal_radius: EqualRadius,
4087 new_ast: &mut ast::Node<ast::Program>,
4088 ) -> Result<AstNodeRef, KclError> {
4089 if equal_radius.input.len() < 2 {
4090 return Err(KclError::refactor(format!(
4091 "equalRadius constraint must have at least 2 segments, got {}",
4092 equal_radius.input.len()
4093 )));
4094 }
4095
4096 let sketch_id = sketch;
4097 let input_asts = equal_radius
4098 .input
4099 .iter()
4100 .map(|segment_id| self.equal_radius_segment_id_to_ast_reference(*segment_id, new_ast))
4101 .collect::<Result<Vec<_>, _>>()?;
4102
4103 let equal_radius_ast = create_equal_radius_ast(input_asts);
4104 let (sketch_block_ref, _) = self.mutate_ast(
4105 new_ast,
4106 sketch_id,
4107 AstMutateCommand::AddSketchBlockExprStmt { expr: equal_radius_ast },
4108 )?;
4109 Ok(sketch_block_ref)
4110 }
4111
4112 async fn add_radius(
4113 &mut self,
4114 sketch: ObjectId,
4115 radius: Radius,
4116 new_ast: &mut ast::Node<ast::Program>,
4117 ) -> Result<AstNodeRef, KclError> {
4118 let params = ArcSizeConstraintParams {
4119 points: vec![radius.arc],
4120 function_name: RADIUS_FN,
4121 value: radius.radius.value,
4122 units: radius.radius.units,
4123 label_position: radius.label_position,
4124 constraint_type_name: "Radius",
4125 };
4126 self.add_arc_size_constraint(sketch, params, new_ast).await
4127 }
4128
4129 async fn add_diameter(
4130 &mut self,
4131 sketch: ObjectId,
4132 diameter: Diameter,
4133 new_ast: &mut ast::Node<ast::Program>,
4134 ) -> Result<AstNodeRef, KclError> {
4135 let params = ArcSizeConstraintParams {
4136 points: vec![diameter.arc],
4137 function_name: DIAMETER_FN,
4138 value: diameter.diameter.value,
4139 units: diameter.diameter.units,
4140 label_position: diameter.label_position,
4141 constraint_type_name: "Diameter",
4142 };
4143 self.add_arc_size_constraint(sketch, params, new_ast).await
4144 }
4145
4146 async fn add_fixed_constraints(
4147 &mut self,
4148 sketch: ObjectId,
4149 points: Vec<FixedPoint>,
4150 new_ast: &mut ast::Node<ast::Program>,
4151 ) -> Result<AstNodeRef, KclError> {
4152 let mut sketch_block_ref = None;
4153
4154 for fixed_point in points {
4155 let point_ast = self.point_id_to_ast_reference(fixed_point.point, new_ast)?;
4156 let fixed_ast = create_fixed_point_constraint_ast(point_ast, fixed_point.position)
4157 .map_err(|err| KclError::refactor(err.to_string()))?;
4158
4159 let (sketch_ref, _) = self.mutate_ast(
4160 new_ast,
4161 sketch,
4162 AstMutateCommand::AddSketchBlockExprStmt { expr: fixed_ast },
4163 )?;
4164 sketch_block_ref = Some(sketch_ref);
4165 }
4166
4167 sketch_block_ref.ok_or_else(|| KclError::refactor("Fixed constraint requires at least one point".to_owned()))
4168 }
4169
4170 async fn add_arc_size_constraint(
4171 &mut self,
4172 sketch: ObjectId,
4173 params: ArcSizeConstraintParams,
4174 new_ast: &mut ast::Node<ast::Program>,
4175 ) -> Result<AstNodeRef, KclError> {
4176 let sketch_id = sketch;
4177
4178 if params.points.len() != 1 {
4180 return Err(KclError::refactor(format!(
4181 "{} constraint must have exactly 1 argument (an arc segment), got {}",
4182 params.constraint_type_name,
4183 params.points.len()
4184 )));
4185 }
4186
4187 let arc_id = params.points[0];
4188 let arc_object = self
4189 .scene_graph
4190 .objects
4191 .get(arc_id.0)
4192 .ok_or_else(|| KclError::refactor(format!("Arc segment not found: {arc_id:?}")))?;
4193 let ObjectKind::Segment { segment: arc_segment } = &arc_object.kind else {
4194 return Err(KclError::refactor(format!("Object is not a segment: {arc_object:?}")));
4195 };
4196 let ref_type = match arc_segment {
4197 Segment::Arc(_) => ARC_VARIABLE,
4198 Segment::Circle(_) => CIRCLE_VARIABLE,
4199 _ => {
4200 return Err(KclError::refactor(format!(
4201 "{} constraint argument must be an arc or circle segment, got: {arc_segment:?}",
4202 params.constraint_type_name
4203 )));
4204 }
4205 };
4206 let arc_ast = get_or_insert_ast_reference(new_ast, &arc_object.source, ref_type, None)?;
4208 let arguments = match ¶ms.label_position {
4209 Some(label_position) => vec![ast::LabeledArg {
4210 label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
4211 arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
4212 }],
4213 None => Default::default(),
4214 };
4215
4216 let call_ast = ast::BinaryPart::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
4218 callee: ast::Node::no_src(ast_sketch2_name(params.function_name)),
4219 unlabeled: Some(arc_ast),
4220 arguments,
4221 digest: None,
4222 non_code_meta: Default::default(),
4223 })));
4224 let constraint_ast = ast::Expr::BinaryExpression(BoxNode::new(ast::Node::no_src(ast::BinaryExpression {
4225 left: call_ast,
4226 operator: ast::BinaryOperator::Eq,
4227 right: ast::BinaryPart::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
4228 value: ast::LiteralValue::Number {
4229 value: params.value,
4230 suffix: params.units,
4231 },
4232 raw: format_number_literal(params.value, params.units, None)
4233 .map_err(|_| KclError::refactor(format!("Could not format numeric suffix: {:?}", params.units)))?,
4234 digest: None,
4235 }))),
4236 digest: None,
4237 })));
4238
4239 let (sketch_block_ref, _) = self.mutate_ast(
4241 new_ast,
4242 sketch_id,
4243 AstMutateCommand::AddSketchBlockExprStmt { expr: constraint_ast },
4244 )?;
4245 Ok(sketch_block_ref)
4246 }
4247
4248 async fn add_horizontal_distance(
4249 &mut self,
4250 sketch: ObjectId,
4251 distance: Distance,
4252 new_ast: &mut ast::Node<ast::Program>,
4253 ) -> Result<AstNodeRef, KclError> {
4254 self.add_distance_constraint(sketch, HORIZONTAL_DISTANCE_FN, distance, new_ast)
4255 }
4256
4257 async fn add_vertical_distance(
4258 &mut self,
4259 sketch: ObjectId,
4260 distance: Distance,
4261 new_ast: &mut ast::Node<ast::Program>,
4262 ) -> Result<AstNodeRef, KclError> {
4263 self.add_distance_constraint(sketch, VERTICAL_DISTANCE_FN, distance, new_ast)
4264 }
4265
4266 async fn add_horizontal(
4267 &mut self,
4268 sketch: ObjectId,
4269 horizontal: Horizontal,
4270 new_ast: &mut ast::Node<ast::Program>,
4271 ) -> Result<AstNodeRef, KclError> {
4272 let sketch_id = sketch;
4273
4274 let first_arg_ast = match horizontal {
4276 Horizontal::Line { line } => {
4277 let line_object = self
4278 .scene_graph
4279 .objects
4280 .get(line.0)
4281 .ok_or_else(|| KclError::refactor(format!("Line not found: {line:?}")))?;
4282 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4283 let kind = line_object.kind.human_friendly_kind_with_article();
4284 return Err(KclError::refactor(format!(
4285 "This constraint only works on Segments, but you selected {kind}"
4286 )));
4287 };
4288 let Segment::Line(_) = line_segment else {
4289 return Err(KclError::refactor(format!(
4290 "Only lines can be made horizontal, but you selected {}",
4291 line_segment.human_friendly_kind_with_article(),
4292 )));
4293 };
4294 self.line_id_to_ast_reference(line, new_ast)?
4295 }
4296 Horizontal::Points { points } => {
4297 let point_asts = points
4298 .iter()
4299 .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
4300 .collect::<Result<Vec<_>, _>>()?;
4301 ast::ArrayExpression::new(point_asts).into()
4302 }
4303 };
4304 let horizontal_ast = create_horizontal_ast(first_arg_ast);
4306
4307 let (sketch_block_ref, _) = self.mutate_ast(
4309 new_ast,
4310 sketch_id,
4311 AstMutateCommand::AddSketchBlockExprStmt { expr: horizontal_ast },
4312 )?;
4313 Ok(sketch_block_ref)
4314 }
4315
4316 async fn add_lines_equal_length(
4317 &mut self,
4318 sketch: ObjectId,
4319 lines_equal_length: LinesEqualLength,
4320 new_ast: &mut ast::Node<ast::Program>,
4321 ) -> Result<AstNodeRef, KclError> {
4322 if lines_equal_length.lines.len() < 2 {
4323 return Err(KclError::refactor(format!(
4324 "Lines equal length constraint must have at least 2 lines, got {}",
4325 lines_equal_length.lines.len()
4326 )));
4327 };
4328
4329 let sketch_id = sketch;
4330
4331 let line_asts = lines_equal_length
4333 .lines
4334 .iter()
4335 .map(|line_id| {
4336 let line_object = self
4337 .scene_graph
4338 .objects
4339 .get(line_id.0)
4340 .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
4341 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4342 let kind = line_object.kind.human_friendly_kind_with_article();
4343 return Err(KclError::refactor(format!(
4344 "This constraint only works on Segments, but you selected {kind}"
4345 )));
4346 };
4347 let Segment::Line(_) = line_segment else {
4348 let kind = line_segment.human_friendly_kind_with_article();
4349 return Err(KclError::refactor(format!(
4350 "Only lines can be made equal length, but you selected {kind}"
4351 )));
4352 };
4353
4354 self.line_id_to_ast_reference(*line_id, new_ast)
4355 })
4356 .collect::<Result<Vec<_>, _>>()?;
4357
4358 let equal_length_ast = create_equal_length_ast(line_asts);
4360
4361 let (sketch_block_ref, _) = self.mutate_ast(
4363 new_ast,
4364 sketch_id,
4365 AstMutateCommand::AddSketchBlockExprStmt { expr: equal_length_ast },
4366 )?;
4367 Ok(sketch_block_ref)
4368 }
4369
4370 fn equal_radius_segment_id_to_ast_reference(
4371 &mut self,
4372 segment_id: ObjectId,
4373 new_ast: &mut ast::Node<ast::Program>,
4374 ) -> Result<ast::Expr, KclError> {
4375 let segment_object = self
4376 .scene_graph
4377 .objects
4378 .get(segment_id.0)
4379 .ok_or_else(|| KclError::refactor(format!("Segment not found: {segment_id:?}")))?;
4380 let ObjectKind::Segment { segment } = &segment_object.kind else {
4381 return Err(KclError::refactor(format!(
4382 "Object is not a segment, it was {}",
4383 segment_object.kind.human_friendly_kind_with_article()
4384 )));
4385 };
4386
4387 let ref_type = match segment {
4388 Segment::Arc(_) => ARC_VARIABLE,
4389 Segment::Circle(_) => CIRCLE_VARIABLE,
4390 _ => {
4391 return Err(KclError::refactor(format!(
4392 "equalRadius supports only arc/circle segments, got {}",
4393 segment.human_friendly_kind_with_article()
4394 )));
4395 }
4396 };
4397
4398 get_or_insert_ast_reference(new_ast, &segment_object.source, ref_type, None)
4399 }
4400
4401 fn symmetric_input_id_to_ast_reference(
4402 &mut self,
4403 segment_id: ObjectId,
4404 new_ast: &mut ast::Node<ast::Program>,
4405 ) -> Result<ast::Expr, KclError> {
4406 let segment_object = self
4407 .scene_graph
4408 .objects
4409 .get(segment_id.0)
4410 .ok_or_else(|| KclError::refactor(format!("Segment not found: {segment_id:?}")))?;
4411 let ObjectKind::Segment { segment } = &segment_object.kind else {
4412 return Err(KclError::refactor(format!(
4413 "Object is not a segment, it was {}",
4414 segment_object.kind.human_friendly_kind_with_article()
4415 )));
4416 };
4417
4418 match segment {
4419 Segment::Point(_) => self.point_id_to_ast_reference(segment_id, new_ast),
4420 Segment::Line(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, LINE_VARIABLE, None),
4421 Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, ARC_VARIABLE, None),
4422 Segment::Circle(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, CIRCLE_VARIABLE, None),
4423 Segment::ControlPointSpline(_) => Err(KclError::refactor(
4424 "Symmetric does not yet support control point splines".to_owned(),
4425 )),
4426 }
4427 }
4428
4429 fn symmetric_axis_id_to_ast_reference(
4430 &mut self,
4431 segment_id: ObjectId,
4432 new_ast: &mut ast::Node<ast::Program>,
4433 ) -> Result<ast::Expr, KclError> {
4434 let segment_object = self
4435 .scene_graph
4436 .objects
4437 .get(segment_id.0)
4438 .ok_or_else(|| KclError::refactor(format!("Axis segment not found: {segment_id:?}")))?;
4439 let ObjectKind::Segment { segment } = &segment_object.kind else {
4440 return Err(KclError::refactor(format!(
4441 "Object is not a segment, it was {}",
4442 segment_object.kind.human_friendly_kind_with_article()
4443 )));
4444 };
4445 match segment {
4446 Segment::Line(_) => self.line_id_to_ast_reference(segment_id, new_ast),
4447 _ => Err(KclError::refactor(format!(
4448 "Symmetric axis must be a line, got {}",
4449 segment.human_friendly_kind_with_article()
4450 ))),
4451 }
4452 }
4453
4454 async fn add_parallel(
4455 &mut self,
4456 sketch: ObjectId,
4457 parallel: Parallel,
4458 new_ast: &mut ast::Node<ast::Program>,
4459 ) -> Result<AstNodeRef, KclError> {
4460 if parallel.lines.len() < 2 {
4461 return Err(KclError::refactor(format!(
4462 "Parallel constraint must have at least 2 lines, got {}",
4463 parallel.lines.len()
4464 )));
4465 };
4466
4467 let sketch_id = sketch;
4468
4469 let line_asts = parallel
4470 .lines
4471 .iter()
4472 .map(|line_id| {
4473 let line_object = self
4474 .scene_graph
4475 .objects
4476 .get(line_id.0)
4477 .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
4478 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4479 let kind = line_object.kind.human_friendly_kind_with_article();
4480 return Err(KclError::refactor(format!(
4481 "This constraint only works on Segments, but you selected {kind}"
4482 )));
4483 };
4484 let Segment::Line(_) = line_segment else {
4485 let kind = line_segment.human_friendly_kind_with_article();
4486 return Err(KclError::refactor(format!(
4487 "Only lines can be made parallel, but you selected {kind}"
4488 )));
4489 };
4490
4491 self.line_id_to_ast_reference(*line_id, new_ast)
4492 })
4493 .collect::<Result<Vec<_>, _>>()?;
4494
4495 let call_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
4496 callee: ast::Node::no_src(ast_sketch2_name(LinesAtAngleKind::Parallel.to_function_name())),
4497 unlabeled: Some(ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(
4498 ast::ArrayExpression {
4499 elements: line_asts,
4500 digest: None,
4501 non_code_meta: Default::default(),
4502 },
4503 )))),
4504 arguments: Default::default(),
4505 digest: None,
4506 non_code_meta: Default::default(),
4507 })));
4508
4509 let (sketch_block_ref, _) = self.mutate_ast(
4510 new_ast,
4511 sketch_id,
4512 AstMutateCommand::AddSketchBlockExprStmt { expr: call_ast },
4513 )?;
4514 Ok(sketch_block_ref)
4515 }
4516
4517 async fn add_perpendicular(
4518 &mut self,
4519 sketch: ObjectId,
4520 perpendicular: Perpendicular,
4521 new_ast: &mut ast::Node<ast::Program>,
4522 ) -> Result<AstNodeRef, KclError> {
4523 self.add_lines_at_angle_constraint(sketch, LinesAtAngleKind::Perpendicular, perpendicular.lines, new_ast)
4524 .await
4525 }
4526
4527 async fn add_lines_at_angle_constraint(
4528 &mut self,
4529 sketch: ObjectId,
4530 angle_kind: LinesAtAngleKind,
4531 lines: Vec<ObjectId>,
4532 new_ast: &mut ast::Node<ast::Program>,
4533 ) -> Result<AstNodeRef, KclError> {
4534 let &[line0_id, line1_id] = lines.as_slice() else {
4535 return Err(KclError::refactor(format!(
4536 "{} constraint must have exactly 2 lines, got {}",
4537 angle_kind.to_function_name(),
4538 lines.len()
4539 )));
4540 };
4541
4542 let sketch_id = sketch;
4543
4544 let line0_object = self
4546 .scene_graph
4547 .objects
4548 .get(line0_id.0)
4549 .ok_or_else(|| KclError::refactor(format!("Line not found: {line0_id:?}")))?;
4550 let ObjectKind::Segment { segment: line0_segment } = &line0_object.kind else {
4551 let kind = line0_object.kind.human_friendly_kind_with_article();
4552 return Err(KclError::refactor(format!(
4553 "This constraint only works on Segments, but you selected {kind}"
4554 )));
4555 };
4556 let Segment::Line(_) = line0_segment else {
4557 return Err(KclError::refactor(format!(
4558 "Only lines can be made {}, but you selected {}",
4559 angle_kind.to_function_name(),
4560 line0_segment.human_friendly_kind_with_article(),
4561 )));
4562 };
4563 let line0_ast = self.line_id_to_ast_reference(line0_id, new_ast)?;
4564
4565 let line1_object = self
4566 .scene_graph
4567 .objects
4568 .get(line1_id.0)
4569 .ok_or_else(|| KclError::refactor(format!("Line not found: {line1_id:?}")))?;
4570 let ObjectKind::Segment { segment: line1_segment } = &line1_object.kind else {
4571 let kind = line1_object.kind.human_friendly_kind_with_article();
4572 return Err(KclError::refactor(format!(
4573 "This constraint only works on Segments, but you selected {kind}"
4574 )));
4575 };
4576 let Segment::Line(_) = line1_segment else {
4577 return Err(KclError::refactor(format!(
4578 "Only lines can be made {}, but you selected {}",
4579 angle_kind.to_function_name(),
4580 line1_segment.human_friendly_kind_with_article(),
4581 )));
4582 };
4583 let line1_ast = self.line_id_to_ast_reference(line1_id, new_ast)?;
4584
4585 let call_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
4587 callee: ast::Node::no_src(ast_sketch2_name(angle_kind.to_function_name())),
4588 unlabeled: Some(ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(
4589 ast::ArrayExpression {
4590 elements: vec![line0_ast, line1_ast],
4591 digest: None,
4592 non_code_meta: Default::default(),
4593 },
4594 )))),
4595 arguments: Default::default(),
4596 digest: None,
4597 non_code_meta: Default::default(),
4598 })));
4599
4600 let (sketch_block_ref, _) = self.mutate_ast(
4602 new_ast,
4603 sketch_id,
4604 AstMutateCommand::AddSketchBlockExprStmt { expr: call_ast },
4605 )?;
4606 Ok(sketch_block_ref)
4607 }
4608
4609 async fn add_vertical(
4610 &mut self,
4611 sketch: ObjectId,
4612 vertical: Vertical,
4613 new_ast: &mut ast::Node<ast::Program>,
4614 ) -> Result<AstNodeRef, KclError> {
4615 let sketch_id = sketch;
4616
4617 let first_arg_ast = match vertical {
4618 Vertical::Line { line } => {
4619 let line_object = self
4621 .scene_graph
4622 .objects
4623 .get(line.0)
4624 .ok_or_else(|| KclError::refactor(format!("Line not found: {line:?}")))?;
4625 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4626 let kind = line_object.kind.human_friendly_kind_with_article();
4627 return Err(KclError::refactor(format!(
4628 "This constraint only works on Segments, but you selected {kind}"
4629 )));
4630 };
4631 let Segment::Line(_) = line_segment else {
4632 return Err(KclError::refactor(format!(
4633 "Only lines can be made vertical, but you selected {}",
4634 line_segment.human_friendly_kind_with_article()
4635 )));
4636 };
4637 self.line_id_to_ast_reference(line, new_ast)?
4638 }
4639 Vertical::Points { points } => {
4640 let point_asts = points
4641 .iter()
4642 .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
4643 .collect::<Result<Vec<_>, _>>()?;
4644 ast::ArrayExpression::new(point_asts).into()
4645 }
4646 };
4647 let vertical_ast = create_vertical_ast(first_arg_ast);
4649
4650 let (sketch_block_ref, _) = self.mutate_ast(
4652 new_ast,
4653 sketch_id,
4654 AstMutateCommand::AddSketchBlockExprStmt { expr: vertical_ast },
4655 )?;
4656 Ok(sketch_block_ref)
4657 }
4658
4659 async fn execute_after_add_constraint(
4660 &mut self,
4661 ctx: &ExecutorContext,
4662 sketch_id: ObjectId,
4663 sketch_block_ref: AstNodeRef,
4664 new_ast: &mut ast::Node<ast::Program>,
4665 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
4666 let new_source = source_from_ast(new_ast);
4668 let new_program = parse_frontend_mutation_source(
4670 &new_source,
4671 "Error parsing KCL source after adding constraint",
4672 "No AST produced after adding constraint",
4673 )?;
4674 let constraint_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
4675 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
4676 "Source range of new constraint not found in sketch block: {sketch_block_ref:?}; {err:?}"
4677 )))
4678 })?;
4679
4680 let mut truncated_program = new_program.clone();
4683 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
4684 .map_err(KclErrorWithOutputs::no_outputs)?;
4685
4686 let outcome = ctx
4688 .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch_id))
4689 .await?;
4690
4691 let new_object_ids = {
4692 let constraint_id = outcome
4694 .source_range_to_object
4695 .get(&constraint_node_ref.range)
4696 .copied()
4697 .ok_or_else(|| {
4698 KclErrorWithOutputs::from_error_outcome(
4699 KclError::refactor(format!("Source range of constraint not found: {constraint_node_ref:?}")),
4700 outcome.clone(),
4701 )
4702 })?;
4703 vec![constraint_id]
4704 };
4705
4706 self.program = new_program;
4709
4710 let outcome = self.update_state_after_exec(outcome, true);
4712
4713 let src_delta = self.commit_var_solutions_to_program(&outcome, "adding constraint")?;
4714 let scene_graph_delta = SceneGraphDelta {
4715 new_graph: self.scene_graph_for_ui(),
4716 invalidates_ids: false,
4717 new_objects: new_object_ids,
4718 exec_outcome: outcome,
4719 };
4720 Ok((src_delta, scene_graph_delta))
4721 }
4722
4723 fn commit_var_solutions_to_program(&mut self, outcome: &ExecOutcome, operation: &str) -> ExecResult<SourceDelta> {
4724 let commit_failure = || {
4725 KclErrorWithOutputs::from_error_outcome(
4726 KclError::refactor(format!("Could not update KCL after {operation}.")),
4727 outcome.clone(),
4728 )
4729 };
4730
4731 let default_length_unit = self.default_length_unit();
4732 let mut settled_ast = self.program.ast.clone();
4733 let mut committed_solver_value = false;
4734 for (var_range, node_path, value) in &outcome.var_solutions {
4735 let Some(lookup) = numeric_literal_at_node_path(&settled_ast, node_path.as_ref(), *var_range) else {
4736 return Err(commit_failure());
4737 };
4738 let new_value = match &lookup {
4739 Some(current_literal) => {
4740 if !var_solution_needs_commit(current_literal, *value, default_length_unit) {
4741 continue;
4742 }
4743 preserve_var_solution_literal_style(current_literal, *value, default_length_unit)
4744 }
4745 None => {
4746 Number {
4750 value: number_value_in_default_length_units(*value, default_length_unit),
4751 units: default_length_unit.into(),
4752 }
4753 }
4754 };
4755 committed_solver_value = true;
4756 let source_ref = SourceRef::Simple {
4757 range: *var_range,
4758 node_path: node_path.clone(),
4759 };
4760 mutate_ast_node_by_source_ref(
4761 &mut settled_ast,
4762 &source_ref,
4763 AstMutateCommand::EditVarInitialValue { value: new_value },
4764 )
4765 .map_err(|_| commit_failure())?;
4766 }
4767
4768 if !committed_solver_value {
4769 return Ok(SourceDelta {
4770 text: self.program.original_file_contents.clone(),
4771 });
4772 }
4773
4774 let settled_source = source_from_ast(&settled_ast);
4775 let (settled_program, errors) = Program::parse(&settled_source).map_err(|_| commit_failure())?;
4776 if !errors.is_empty() {
4777 return Err(commit_failure());
4778 }
4779 let Some(settled_program) = settled_program else {
4780 return Err(commit_failure());
4781 };
4782
4783 self.program = settled_program;
4784
4785 Ok(SourceDelta { text: settled_source })
4786 }
4787
4788 fn segment_will_be_deleted(&self, segment_id: ObjectId, segment_ids_set: &AhashIndexSet<ObjectId>) -> bool {
4790 if segment_ids_set.contains(&segment_id) {
4791 return true;
4792 }
4793
4794 let Some(segment_object) = self.scene_graph.objects.get(segment_id.0) else {
4795 return false;
4796 };
4797 let ObjectKind::Segment { segment } = &segment_object.kind else {
4798 return false;
4799 };
4800 let Segment::Point(point) = segment else {
4801 return false;
4802 };
4803
4804 point.owner.is_some_and(|owner_id| segment_ids_set.contains(&owner_id))
4805 }
4806
4807 fn remaining_constraint_segments(
4808 &self,
4809 segments: &[ConstraintSegment],
4810 segment_ids_set: &AhashIndexSet<ObjectId>,
4811 ) -> Vec<ConstraintSegment> {
4812 segments
4813 .iter()
4814 .copied()
4815 .filter(|segment| match segment {
4816 ConstraintSegment::Origin(_) => true,
4817 ConstraintSegment::Segment(segment_id) => !self.segment_will_be_deleted(*segment_id, segment_ids_set),
4818 })
4819 .collect()
4820 }
4821
4822 fn find_referenced_constraints(
4823 &self,
4824 sketch_id: ObjectId,
4825 segment_ids_set: &AhashIndexSet<ObjectId>,
4826 ) -> Result<AhashIndexSet<ObjectId>, KclError> {
4827 let sketch_object = self
4829 .scene_graph
4830 .objects
4831 .get(sketch_id.0)
4832 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch_id:?}")))?;
4833 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
4834 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
4835 };
4836 let segment_or_owner_matches = |segment_id: ObjectId| {
4837 if segment_ids_set.contains(&segment_id) {
4838 return true;
4839 }
4840 let segment_object = self.scene_graph.objects.get(segment_id.0);
4841 if let Some(obj) = segment_object
4842 && let ObjectKind::Segment { segment } = &obj.kind
4843 {
4844 match segment {
4845 Segment::Point(point) => point.owner.is_some_and(|owner_id| segment_ids_set.contains(&owner_id)),
4846 Segment::Line(line) => line.owner.is_some_and(|owner_id| segment_ids_set.contains(&owner_id)),
4847 _ => false,
4848 }
4849 } else {
4850 false
4851 }
4852 };
4853 let mut constraint_ids_set = AhashIndexSet::default();
4854 for constraint_id in &sketch.constraints {
4855 let constraint_object = self
4856 .scene_graph
4857 .objects
4858 .get(constraint_id.0)
4859 .ok_or_else(|| KclError::refactor(format!("Constraint not found: {constraint_id:?}")))?;
4860 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
4861 return Err(KclError::refactor(format!(
4862 "Object is not a constraint, it is {}",
4863 constraint_object.kind.human_friendly_kind_with_article()
4864 )));
4865 };
4866 let depends_on_segment = match constraint {
4867 Constraint::Coincident(c) => c.segment_ids().any(segment_or_owner_matches),
4868 Constraint::Distance(d) => d.segment_ids().any(segment_or_owner_matches),
4869 Constraint::Fixed(fixed) => fixed
4870 .points
4871 .iter()
4872 .any(|fixed_point| self.segment_will_be_deleted(fixed_point.point, segment_ids_set)),
4873 Constraint::Radius(r) => segment_or_owner_matches(r.arc),
4874 Constraint::Diameter(d) => segment_or_owner_matches(d.arc),
4875 Constraint::EqualRadius(equal_radius) => {
4876 equal_radius.input.iter().copied().any(segment_or_owner_matches)
4877 }
4878 Constraint::HorizontalDistance(d) => d.segment_ids().any(segment_or_owner_matches),
4879 Constraint::VerticalDistance(d) => d.segment_ids().any(segment_or_owner_matches),
4880 Constraint::Horizontal(h) => match h {
4881 Horizontal::Line { line } => segment_or_owner_matches(*line),
4882 Horizontal::Points { points } => points.iter().any(|point| match point {
4883 ConstraintSegment::Segment(point) => segment_or_owner_matches(*point),
4884 ConstraintSegment::Origin(_) => false,
4885 }),
4886 },
4887 Constraint::Vertical(v) => match v {
4888 Vertical::Line { line } => segment_or_owner_matches(*line),
4889 Vertical::Points { points } => points.iter().any(|point| match point {
4890 ConstraintSegment::Segment(point) => segment_or_owner_matches(*point),
4891 ConstraintSegment::Origin(_) => false,
4892 }),
4893 },
4894 Constraint::LinesEqualLength(lines_equal_length) => {
4895 lines_equal_length.lines.iter().copied().any(segment_or_owner_matches)
4896 }
4897 Constraint::Midpoint(midpoint) => {
4898 segment_or_owner_matches(midpoint.segment)
4899 || matches!(
4900 midpoint.point,
4901 ConstraintSegment::Segment(point) if segment_or_owner_matches(point)
4902 )
4903 }
4904 Constraint::Parallel(parallel) => parallel.lines.iter().copied().any(segment_or_owner_matches),
4905 Constraint::Perpendicular(perpendicular) => {
4906 perpendicular.lines.iter().copied().any(segment_or_owner_matches)
4907 }
4908 Constraint::Angle(angle) => angle.lines.iter().copied().any(segment_or_owner_matches),
4909 Constraint::Symmetric(symmetric) => {
4910 segment_or_owner_matches(symmetric.axis)
4911 || symmetric.input.iter().copied().any(segment_or_owner_matches)
4912 }
4913 Constraint::Tangent(tangent) => tangent.input.iter().copied().any(segment_or_owner_matches),
4914 };
4915 if depends_on_segment {
4916 constraint_ids_set.insert(*constraint_id);
4917 }
4918 }
4919 Ok(constraint_ids_set)
4920 }
4921
4922 fn update_state_after_exec(&mut self, outcome: ExecOutcome, freedom_analysis_ran: bool) -> ExecOutcome {
4923 let mut outcome = outcome;
4924 self.solid_references = solid_references_from_variables(&self.program.ast, &outcome.variables);
4925 let mut new_objects = std::mem::take(&mut outcome.scene_objects);
4926
4927 if freedom_analysis_ran {
4928 self.point_freedom_cache.clear();
4931 for new_obj in &new_objects {
4932 if let ObjectKind::Segment {
4933 segment: kcl_api::Segment::Point(point),
4934 } = &new_obj.kind
4935 {
4936 self.point_freedom_cache.insert(new_obj.id, point.freedom);
4937 }
4938 }
4939 add_wall_and_cap_face_objects(&mut new_objects, &outcome.artifact_graph);
4940 self.scene_graph.objects = new_objects;
4942 } else {
4943 for old_obj in &self.scene_graph.objects {
4946 if let ObjectKind::Segment {
4947 segment: kcl_api::Segment::Point(point),
4948 } = &old_obj.kind
4949 {
4950 self.point_freedom_cache.insert(old_obj.id, point.freedom);
4951 }
4952 }
4953
4954 let mut updated_objects = Vec::with_capacity(new_objects.len());
4956 for new_obj in new_objects {
4957 let mut obj = new_obj;
4958 if let ObjectKind::Segment {
4959 segment: kcl_api::Segment::Point(point),
4960 } = &mut obj.kind
4961 {
4962 let new_freedom = point.freedom;
4963 match new_freedom {
4969 Freedom::Free => {
4970 match self.point_freedom_cache.get(&obj.id).copied() {
4971 Some(Freedom::Conflict) => {
4972 }
4975 Some(Freedom::Fixed) => {
4976 point.freedom = Freedom::Fixed;
4978 }
4979 Some(Freedom::Free) => {
4980 }
4982 None => {
4983 }
4985 }
4986 }
4987 Freedom::Fixed => {
4988 }
4990 Freedom::Conflict => {
4991 }
4993 }
4994 self.point_freedom_cache.insert(obj.id, point.freedom);
4996 }
4997 updated_objects.push(obj);
4998 }
4999
5000 add_wall_and_cap_face_objects(&mut updated_objects, &outcome.artifact_graph);
5001 self.scene_graph.objects = updated_objects;
5002 }
5003 outcome
5004 }
5005
5006 fn mutate_ast(
5007 &mut self,
5008 ast: &mut ast::Node<ast::Program>,
5009 object_id: ObjectId,
5010 command: AstMutateCommand,
5011 ) -> Result<(AstNodeRef, AstMutateCommandReturn), KclError> {
5012 let sketch_object = self
5013 .scene_graph
5014 .objects
5015 .get(object_id.0)
5016 .ok_or_else(|| KclError::refactor(format!("Object not found: {object_id:?}")))?;
5017 mutate_ast_node_by_source_ref(ast, &sketch_object.source, command)
5018 }
5019
5020 fn mutate_constraint_label_position(
5021 &mut self,
5022 ast: &mut ast::Node<ast::Program>,
5023 constraint_id: ObjectId,
5024 label_position: Point2d<Number>,
5025 ) -> Result<(), KclError> {
5026 let object = self
5027 .scene_graph
5028 .objects
5029 .get(constraint_id.0)
5030 .ok_or_else(|| KclError::refactor(format!("Object not found: {constraint_id:?}")))?;
5031 if !matches!(
5032 &object.kind,
5033 ObjectKind::Constraint {
5034 constraint: Constraint::Distance(_)
5035 | Constraint::HorizontalDistance(_)
5036 | Constraint::VerticalDistance(_)
5037 | Constraint::Radius(_)
5038 | Constraint::Diameter(_)
5039 | Constraint::Angle(_),
5040 }
5041 ) {
5042 return Err(KclError::refactor(format!(
5043 "Object does not support labelPosition: {constraint_id:?}"
5044 )));
5045 }
5046
5047 let label_position = to_ast_point2d_number(&label_position)
5048 .map_err(|err| KclError::refactor(format!("Could not convert label position to AST: {err}")))?;
5049 self.mutate_ast(
5050 ast,
5051 constraint_id,
5052 AstMutateCommand::EditDistanceConstraintLabelPosition { label_position },
5053 )?;
5054 Ok(())
5055 }
5056}
5057
5058fn sketch_block_ref_from_id(scene_graph: &SceneGraph, sketch_id: ObjectId) -> Result<AstNodeRef, KclError> {
5059 let sketch_object = scene_graph
5061 .objects
5062 .get(sketch_id.0)
5063 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch_id:?}")))?;
5064 let ObjectKind::Sketch(_) = &sketch_object.kind else {
5065 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
5066 };
5067 expect_single_node_ref(sketch_object)
5068}
5069
5070fn expect_single_node_ref(object: &Object) -> Result<AstNodeRef, KclError> {
5071 match &object.source {
5072 SourceRef::Simple { range, node_path } => Ok(AstNodeRef {
5073 range: *range,
5074 node_path: node_path.clone(),
5075 }),
5076 SourceRef::BackTrace { ranges } => {
5077 let [range] = ranges.as_slice() else {
5078 return Err(KclError::refactor(format!(
5079 "Expected single location in SourceRef, got {}; ranges={ranges:#?}",
5080 ranges.len()
5081 )));
5082 };
5083 Ok(AstNodeRef {
5084 range: range.0,
5085 node_path: range.1.clone(),
5086 })
5087 }
5088 }
5089}
5090
5091fn only_sketch_block_from_range(
5094 ast: &mut ast::Node<ast::Program>,
5095 sketch_block_range: SourceRange,
5096 edit_kind: ChangeKind,
5097) -> Result<(), KclError> {
5098 let r1 = sketch_block_range;
5099 let matches_range = |r2: SourceRange| -> bool {
5100 match edit_kind {
5103 ChangeKind::Add => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() <= r2.end(),
5104 ChangeKind::Edit => r1.module_id() == r2.module_id() && r1.start() == r2.start(),
5106 ChangeKind::Delete => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() >= r2.end(),
5107 ChangeKind::None => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() == r2.end(),
5109 }
5110 };
5111 let mut found = false;
5112 for item in ast.body.iter_mut() {
5113 match item {
5114 ast::BodyItem::ImportStatement(_) => {}
5115 ast::BodyItem::ExpressionStatement(node) => {
5116 if matches_range(SourceRange::from(&*node))
5117 && let ast::Expr::SketchBlock(sketch_block) = &mut node.expression
5118 {
5119 sketch_block.is_being_edited = true;
5120 found = true;
5121 break;
5122 }
5123 }
5124 ast::BodyItem::VariableDeclaration(node) => {
5125 if matches_range(SourceRange::from(&node.declaration.init))
5126 && let ast::Expr::SketchBlock(sketch_block) = &mut node.declaration.init
5127 {
5128 sketch_block.is_being_edited = true;
5129 found = true;
5130 break;
5131 }
5132 }
5133 ast::BodyItem::TypeDeclaration(_) => {}
5134 ast::BodyItem::ReturnStatement(node) => {
5135 if matches_range(SourceRange::from(&node.argument))
5136 && let ast::Expr::SketchBlock(sketch_block) = &mut node.argument
5137 {
5138 sketch_block.is_being_edited = true;
5139 found = true;
5140 break;
5141 }
5142 }
5143 }
5144 }
5145 if !found {
5146 return Err(KclError::refactor(format!(
5147 "Sketch block source range not found in AST: {sketch_block_range:?}, edit_kind={edit_kind:?}"
5148 )));
5149 }
5150
5151 Ok(())
5152}
5153
5154fn only_sketch_block(
5155 ast: &mut ast::Node<ast::Program>,
5156 sketch_block_ref: &AstNodeRef,
5157 edit_kind: ChangeKind,
5158) -> Result<(), KclError> {
5159 let Some(target_node_path) = &sketch_block_ref.node_path else {
5160 #[cfg(target_arch = "wasm32")]
5161 web_sys::console::warn_1(
5162 &format!(
5163 "only_sketch_block: target sketch block ref doesn't have node path; sketch_block_ref={:#?}, edit_kind={edit_kind:#?}",
5164 sketch_block_ref
5165 )
5166 .into(),
5167 );
5168 return only_sketch_block_from_range(ast, sketch_block_ref.range, edit_kind);
5169 };
5170 struct MarkSketchBlockBeingEdited<'a> {
5171 target_node_path: &'a ast::NodePath,
5172 }
5173
5174 impl Visitor for MarkSketchBlockBeingEdited<'_> {
5175 type Break = ();
5176 type Continue = ();
5177
5178 fn visit(&mut self, node: NodeMut<'_>) -> TraversalReturn<Self::Break, Self::Continue> {
5179 if let NodeMut::SketchBlock(sketch_block) = node
5180 && sketch_block.node_path.as_ref() == Some(self.target_node_path)
5181 {
5182 sketch_block.is_being_edited = true;
5183 return TraversalReturn::new_break(());
5184 }
5185 TraversalReturn::new_continue(())
5186 }
5187
5188 fn finish(&mut self, _node: NodeMut<'_>) {}
5189 }
5190
5191 let mut marker = MarkSketchBlockBeingEdited { target_node_path };
5192 let found = dfs_mut(ast, &mut marker).is_break();
5193 if !found {
5194 return Err(KclError::refactor(format!(
5195 "Sketch block node path not found in AST: {sketch_block_ref:?}, edit_kind={edit_kind:?}"
5196 )));
5197 }
5198
5199 Ok(())
5200}
5201
5202fn sketch_on_ast_expr(
5203 ast: &mut ast::Node<ast::Program>,
5204 scene_graph: &SceneGraph,
5205 solid_references: &HashMap<Uuid, SolidAstReference>,
5206 on: &Plane,
5207) -> Result<ast::Expr, KclError> {
5208 match on {
5209 Plane::Default(name) => Ok(default_plane_ast_expr(*name)),
5210 Plane::Object(object_id) => {
5211 let on_object = scene_graph
5212 .objects
5213 .get(object_id.0)
5214 .ok_or_else(|| KclError::refactor(format!("Sketch plane object not found: {object_id:?}")))?;
5215 if let Some(face_expr) = sketch_face_of_scene_object_ast_expr(ast, on_object)? {
5216 return Ok(face_expr);
5217 }
5218 get_or_insert_ast_reference(ast, &on_object.source, "plane", None)
5219 }
5220 Plane::PrimitiveFace(face) => {
5221 let solid_expr = solid_expr_for_engine_id(solid_references, face.solid_id).ok_or_else(|| {
5222 KclError::refactor(format!(
5223 "Could not resolve a KCL solid for selected primitive face: solid_id={}",
5224 face.solid_id
5225 ))
5226 })?;
5227 let face_id_expr = create_face_id_ast(solid_expr.clone(), face.index);
5228 Ok(create_face_of_ast(solid_expr, face_id_expr))
5229 }
5230 }
5231}
5232
5233fn solid_references_from_variables(
5234 ast: &ast::Node<ast::Program>,
5235 variables: &IndexMap<String, KclValueView>,
5236) -> HashMap<Uuid, SolidAstReference> {
5237 let mut references = HashMap::new();
5238
5239 for item in &ast.body {
5242 let ast::BodyItem::VariableDeclaration(declaration) = item else {
5243 continue;
5244 };
5245 let name = &declaration.declaration.id.name;
5246 let Some(value) = variables.get(name) else {
5247 continue;
5248 };
5249
5250 match value {
5251 KclValueView::Solid { value } => {
5252 references.insert(
5253 value.id,
5254 SolidAstReference {
5255 variable_name: name.clone(),
5256 output_index: None,
5257 },
5258 );
5259 }
5260 KclValueView::Tuple { value } | KclValueView::HomArray { value } => {
5261 for (output_index, entry) in value.iter().enumerate() {
5262 if let KclValueView::Solid { value } = entry {
5263 references.insert(
5264 value.id,
5265 SolidAstReference {
5266 variable_name: name.clone(),
5267 output_index: Some(output_index),
5268 },
5269 );
5270 }
5271 }
5272 }
5273 _ => {}
5274 }
5275 }
5276
5277 references
5278}
5279
5280fn solid_expr_for_engine_id(solid_references: &HashMap<Uuid, SolidAstReference>, solid_id: Uuid) -> Option<ast::Expr> {
5281 let reference = solid_references.get(&solid_id)?;
5282 let solid_expr = ast_name_expr(reference.variable_name.clone());
5283 Some(indexed_solid_expr_for_sweep_output(solid_expr, reference.output_index))
5284}
5285
5286fn sketch_face_of_scene_object_ast_expr(
5287 ast: &mut ast::Node<ast::Program>,
5288 on_object: &kcl_api::Object,
5289) -> Result<Option<ast::Expr>, KclError> {
5290 match &on_object.kind {
5291 ObjectKind::Wall(wall) => {
5292 let solid_ref = get_or_insert_ast_reference(
5293 ast,
5294 &source_ref_from_source_ref_range(&wall.source.solid),
5295 "solid",
5296 None,
5297 )?;
5298 let ast::Expr::Name(solid_name_expr) = solid_ref else {
5299 return Err(KclError::refactor(format!(
5300 "Could not resolve solid reference for selected wall: artifact_id={:?}",
5301 on_object.artifact_id
5302 )));
5303 };
5304 let solid_expr = indexed_solid_expr_for_sweep_output(
5305 ast_name_expr(solid_name_expr.name.name.clone()),
5306 wall.solid_output_index,
5307 );
5308 let sweep_ref = get_or_insert_ast_reference(
5309 ast,
5310 &source_ref_from_source_ref_range(&wall.source.sweep),
5311 "solid",
5312 None,
5313 )?;
5314 let ast::Expr::Name(sweep_name_expr) = sweep_ref else {
5315 return Err(KclError::refactor(format!(
5316 "Could not resolve sweep reference for selected wall: artifact_id={:?}",
5317 on_object.artifact_id
5318 )));
5319 };
5320 let sweep_name = sweep_name_expr.name.name.clone();
5321 let segment_ref = get_or_insert_ast_reference(
5322 ast,
5323 &source_ref_from_source_ref_range(&wall.source.segment),
5324 LINE_VARIABLE,
5325 None,
5326 )?;
5327
5328 let face_expr = if let Some(region_name) = region_name_from_sweep_variable(ast, &sweep_name).or_else(|| {
5329 wall.source
5330 .path
5331 .as_ref()
5332 .and_then(|path_source| region_name_from_path_source(ast, path_source))
5333 }) {
5334 let ast::Expr::Name(segment_name_expr) = segment_ref else {
5335 return Err(KclError::refactor(format!(
5336 "Could not resolve source segment reference for selected region wall: artifact_id={:?}",
5337 on_object.artifact_id
5338 )));
5339 };
5340 create_member_expression(
5341 create_member_expression(ast_name_expr(region_name), "tags"),
5342 &segment_name_expr.name.name,
5343 )
5344 } else {
5345 segment_ref
5346 };
5347
5348 Ok(Some(create_face_of_ast(solid_expr, face_expr)))
5349 }
5350 ObjectKind::Cap(cap) => {
5351 let solid_ref =
5352 get_or_insert_ast_reference(ast, &source_ref_from_source_ref_range(&cap.source.solid), "solid", None)?;
5353 let ast::Expr::Name(solid_name_expr) = solid_ref else {
5354 return Err(KclError::refactor(format!(
5355 "Could not resolve solid reference for selected cap: artifact_id={:?}",
5356 on_object.artifact_id
5357 )));
5358 };
5359 let solid_expr = indexed_solid_expr_for_sweep_output(
5360 ast_name_expr(solid_name_expr.name.name.clone()),
5361 cap.solid_output_index,
5362 );
5363 let face_expr = match cap.kind {
5365 kcl_api::CapKind::Start => ast_name_expr("START".to_owned()),
5366 kcl_api::CapKind::End => ast_name_expr("END".to_owned()),
5367 };
5368
5369 Ok(Some(create_face_of_ast(solid_expr, face_expr)))
5370 }
5371 _ => Ok(None),
5372 }
5373}
5374
5375fn indexed_solid_expr_for_sweep_output(solid_expr: ast::Expr, solid_output_index: Option<usize>) -> ast::Expr {
5376 match solid_output_index {
5377 Some(output_index) => create_index_expression(solid_expr, output_index),
5378 None => solid_expr,
5379 }
5380}
5381
5382fn source_ref_from_source_ref_range(source: &SourceRefRange) -> SourceRef {
5383 SourceRef::Simple {
5384 range: source.range,
5385 node_path: source.node_path.clone(),
5386 }
5387}
5388
5389fn region_name_from_path_source(ast: &ast::Node<ast::Program>, path_source: &SourceRefRange) -> Option<String> {
5390 let source_ref = source_ref_from_source_ref_range(path_source);
5391 let candidate = variable_name_containing_source_ref(ast, &source_ref)?;
5392 let ast::Definition::Variable(region_decl) = ast.get_variable(&candidate)? else {
5393 return None;
5394 };
5395 let ast::Expr::CallExpressionKw(region_call) = ®ion_decl.init else {
5396 return None;
5397 };
5398 if region_call.callee.name.name != "region" {
5399 return None;
5400 }
5401 Some(candidate)
5402}
5403
5404fn downstream_composite_code_ref_for_source(artifact_graph: &ArtifactGraph, source_id: ArtifactId) -> Option<&CodeRef> {
5405 let mut current_id = source_id;
5406 let mut current_composite = None;
5407 let mut visited = HashSet::new();
5408
5409 while visited.insert(current_id) {
5410 let next_composite_id = downstream_composite_id_for_solid_source(artifact_graph, current_id);
5411
5412 let Some(composite_id) = next_composite_id else {
5413 break;
5414 };
5415 let Some(Artifact::CompositeSolid(composite)) = artifact_graph.get(&composite_id) else {
5416 break;
5417 };
5418
5419 current_id = composite.id;
5420 current_composite = Some(composite);
5421
5422 if !composite.consumed {
5423 break;
5424 }
5425 }
5426
5427 current_composite.map(|composite| &composite.code_ref)
5428}
5429
5430fn downstream_composite_id_for_solid_source(
5431 artifact_graph: &ArtifactGraph,
5432 source_id: ArtifactId,
5433) -> Option<ArtifactId> {
5434 if let Some(Artifact::Path(path)) = artifact_graph.get(&source_id)
5436 && let Some(composite_id) = path.composite_solid_id
5437 && let Some(Artifact::CompositeSolid(composite)) = artifact_graph.get(&composite_id)
5438 && composite_contains_path_input(&composite.solid_ids, &composite.tool_ids, path.id, path.solid2d_id)
5439 {
5440 return Some(composite_id);
5441 }
5442
5443 for artifact in artifact_graph.values() {
5445 if let Artifact::Path(path) = artifact
5446 && path.sweep_id == Some(source_id)
5447 && let Some(composite_id) = path.composite_solid_id
5448 && let Some(Artifact::CompositeSolid(composite)) = artifact_graph.get(&composite_id)
5449 && composite_contains_path_input(&composite.solid_ids, &composite.tool_ids, path.id, path.solid2d_id)
5450 {
5451 return Some(composite_id);
5452 }
5453 }
5454
5455 artifact_graph.values().find_map(|artifact| {
5457 let Artifact::CompositeSolid(composite) = artifact else {
5458 return None;
5459 };
5460 composite_contains_input(&composite.solid_ids, &composite.tool_ids, source_id).then_some(composite.id)
5461 })
5462}
5463
5464fn composite_contains_path_input(
5465 solid_ids: &[ArtifactId],
5466 tool_ids: &[ArtifactId],
5467 path_id: ArtifactId,
5468 solid2d_id: Option<ArtifactId>,
5469) -> bool {
5470 composite_contains_input(solid_ids, tool_ids, path_id)
5471 || solid2d_id.is_some_and(|solid2d_id| composite_contains_input(solid_ids, tool_ids, solid2d_id))
5472}
5473
5474fn composite_contains_input(solid_ids: &[ArtifactId], tool_ids: &[ArtifactId], input_id: ArtifactId) -> bool {
5475 solid_ids.contains(&input_id) || tool_ids.contains(&input_id)
5476}
5477
5478fn code_ref_source_ref_range(code_ref: &CodeRef) -> SourceRefRange {
5479 let node_path = (!code_ref.node_path.is_empty()).then(|| code_ref.node_path.clone());
5480 SourceRefRange {
5481 range: code_ref.range,
5482 node_path,
5483 }
5484}
5485
5486fn solid_output_index_for_sweep(
5487 artifact_graph: &ArtifactGraph,
5488 sweep_id: ArtifactId,
5489 sweep_code_ref: &CodeRef,
5490) -> Option<usize> {
5491 if downstream_composite_id_for_solid_source(artifact_graph, sweep_id).is_some() {
5494 return None;
5495 }
5496
5497 let sibling_sweeps = artifact_graph
5498 .values()
5499 .filter_map(|artifact| match artifact {
5500 Artifact::Sweep(sweep)
5501 if sweep.code_ref.range == sweep_code_ref.range
5502 && sweep.code_ref.node_path == sweep_code_ref.node_path =>
5503 {
5504 Some(sweep)
5505 }
5506 _ => None,
5507 })
5508 .collect::<Vec<_>>();
5509
5510 if sibling_sweeps.len() <= 1 {
5511 return None;
5512 }
5513
5514 sibling_sweeps
5515 .iter()
5516 .position(|sibling_sweep| sibling_sweep.id == sweep_id)
5517}
5518
5519fn add_wall_and_cap_face_objects(scene_objects: &mut Vec<kcl_api::Object>, artifact_graph: &ArtifactGraph) {
5520 let mut existing_artifact_ids = scene_objects
5521 .iter()
5522 .map(|object| object.artifact_id)
5523 .collect::<HashSet<_>>();
5524
5525 for artifact in artifact_graph.values() {
5526 match artifact {
5527 Artifact::Wall(wall) => {
5528 if existing_artifact_ids.contains(&wall.id) {
5529 continue;
5530 }
5531
5532 let Some(segment) = artifact_graph.get(&wall.seg_id).and_then(|artifact| match artifact {
5533 Artifact::Segment(segment) => Some(segment),
5534 _ => None,
5535 }) else {
5536 continue;
5537 };
5538 let Some(sweep) = artifact_graph.get(&wall.sweep_id).and_then(|artifact| match artifact {
5539 Artifact::Sweep(sweep) => Some(sweep),
5540 _ => None,
5541 }) else {
5542 continue;
5543 };
5544 let source_segment = segment
5545 .original_seg_id
5546 .and_then(|original_seg_id| artifact_graph.get(&original_seg_id))
5547 .and_then(|artifact| match artifact {
5548 Artifact::Segment(segment) => Some(segment),
5549 _ => None,
5550 })
5551 .unwrap_or(segment);
5552 let solid_code_ref =
5553 downstream_composite_code_ref_for_source(artifact_graph, wall.sweep_id).unwrap_or(&sweep.code_ref);
5554 let path_code_ref = artifact_graph
5555 .get(&segment.path_id)
5556 .or_else(|| sweep.path_id.and_then(|path_id| artifact_graph.get(&path_id)))
5557 .and_then(|artifact| match artifact {
5558 Artifact::Path(path) => Some(&path.code_ref),
5559 _ => None,
5560 });
5561 let source = WallSource {
5562 solid: code_ref_source_ref_range(solid_code_ref),
5563 sweep: code_ref_source_ref_range(&sweep.code_ref),
5564 path: path_code_ref.map(code_ref_source_ref_range),
5565 segment: code_ref_source_ref_range(&source_segment.code_ref),
5566 };
5567 let solid_output_index = (solid_code_ref.range == sweep.code_ref.range
5568 && solid_code_ref.node_path == sweep.code_ref.node_path)
5569 .then(|| solid_output_index_for_sweep(artifact_graph, sweep.id, &sweep.code_ref))
5570 .flatten();
5571 let object_source = source_ref_from_source_ref_range(&source.solid);
5572 let id = ObjectId(scene_objects.len());
5573 scene_objects.push(kcl_api::Object {
5574 id,
5575 kind: ObjectKind::Wall(kcl_api::Wall {
5576 id,
5577 source,
5578 solid_output_index,
5579 }),
5580 label: Default::default(),
5581 comments: Default::default(),
5582 artifact_id: wall.id,
5583 source: object_source,
5584 });
5585 existing_artifact_ids.insert(wall.id);
5586 }
5587 Artifact::Cap(cap) => {
5588 if existing_artifact_ids.contains(&cap.id) {
5589 continue;
5590 }
5591
5592 let Some(sweep) = artifact_graph.get(&cap.sweep_id).and_then(|artifact| match artifact {
5593 Artifact::Sweep(sweep) => Some(sweep),
5594 _ => None,
5595 }) else {
5596 continue;
5597 };
5598 let id = ObjectId(scene_objects.len());
5599 let kind = match cap.sub_type {
5600 CapSubType::Start => kcl_api::CapKind::Start,
5601 CapSubType::End => kcl_api::CapKind::End,
5602 };
5603 let solid_code_ref =
5604 downstream_composite_code_ref_for_source(artifact_graph, cap.sweep_id).unwrap_or(&sweep.code_ref);
5605 let source = CapSource {
5606 solid: code_ref_source_ref_range(solid_code_ref),
5607 sweep: code_ref_source_ref_range(&sweep.code_ref),
5608 };
5609 let solid_output_index = (solid_code_ref.range == sweep.code_ref.range
5610 && solid_code_ref.node_path == sweep.code_ref.node_path)
5611 .then(|| solid_output_index_for_sweep(artifact_graph, sweep.id, &sweep.code_ref))
5612 .flatten();
5613 let object_source = source_ref_from_source_ref_range(&source.solid);
5614 scene_objects.push(kcl_api::Object {
5615 id,
5616 kind: ObjectKind::Cap(kcl_api::Cap {
5617 id,
5618 kind,
5619 source,
5620 solid_output_index,
5621 }),
5622 label: Default::default(),
5623 comments: Default::default(),
5624 artifact_id: cap.id,
5625 source: object_source,
5626 });
5627 existing_artifact_ids.insert(cap.id);
5628 }
5629 _ => {}
5630 }
5631 }
5632}
5633
5634fn default_plane_ast_expr(name: crate::engine::PlaneName) -> ast::Expr {
5635 use crate::engine::PlaneName;
5636
5637 match name {
5638 PlaneName::Xy => ast_name_expr("XY".to_owned()),
5639 PlaneName::Xz => ast_name_expr("XZ".to_owned()),
5640 PlaneName::Yz => ast_name_expr("YZ".to_owned()),
5641 PlaneName::NegXy => negated_plane_ast_expr("XY"),
5642 PlaneName::NegXz => negated_plane_ast_expr("XZ"),
5643 PlaneName::NegYz => negated_plane_ast_expr("YZ"),
5644 }
5645}
5646
5647fn negated_plane_ast_expr(name: &str) -> ast::Expr {
5648 ast::Expr::UnaryExpression(BoxNode::new(ast::UnaryExpression::new(
5649 ast::UnaryOperator::Neg,
5650 ast::BinaryPart::Name(BoxNode::new(ast_name(name.to_owned()))),
5651 )))
5652}
5653
5654fn create_face_of_ast(solid_expr: ast::Expr, face_expr: ast::Expr) -> ast::Expr {
5655 ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
5656 callee: ast::Node::no_src(ast_sketch2_name("faceOf")),
5657 unlabeled: Some(solid_expr),
5658 arguments: vec![ast::LabeledArg {
5659 label: Some(ast::Identifier::new("face")),
5660 arg: face_expr,
5661 }],
5662 digest: None,
5663 non_code_meta: Default::default(),
5664 })))
5665}
5666
5667fn create_face_id_ast(solid_expr: ast::Expr, index: usize) -> ast::Expr {
5668 ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
5669 callee: ast::Node::no_src(ast_sketch2_name("faceId")),
5670 unlabeled: Some(solid_expr),
5671 arguments: vec![ast::LabeledArg {
5672 label: Some(ast::Identifier::new("index")),
5673 arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal::from(
5674 ast::NumericLiteral {
5675 value: index as f64,
5676 suffix: NumericSuffix::None,
5677 raw: index.to_string(),
5678 digest: None,
5679 },
5680 )))),
5681 }],
5682 digest: None,
5683 non_code_meta: Default::default(),
5684 })))
5685}
5686
5687fn region_name_from_sweep_variable(ast: &ast::Node<ast::Program>, sweep_variable_name: &str) -> Option<String> {
5688 let ast::Definition::Variable(sweep_decl) = ast.get_variable(sweep_variable_name)? else {
5689 return None;
5690 };
5691 let ast::Expr::CallExpressionKw(sweep_call) = &sweep_decl.init else {
5692 return None;
5693 };
5694 if !matches!(
5695 sweep_call.callee.name.name.as_str(),
5696 "extrude" | "revolve" | "sweep" | "loft"
5697 ) {
5698 return None;
5699 }
5700 let ast::Expr::Name(region_name_expr) = sweep_call.unlabeled.as_ref()? else {
5701 return None;
5702 };
5703 let candidate = region_name_expr.name.name.clone();
5704 let ast::Definition::Variable(region_decl) = ast.get_variable(&candidate)? else {
5705 return None;
5706 };
5707 let ast::Expr::CallExpressionKw(region_call) = ®ion_decl.init else {
5708 return None;
5709 };
5710 if region_call.callee.name.name != "region" {
5711 return None;
5712 }
5713 Some(candidate)
5714}
5715
5716fn get_or_insert_ast_reference(
5723 ast: &mut ast::Node<ast::Program>,
5724 source_ref: &SourceRef,
5725 prefix: &str,
5726 property: Option<&str>,
5727) -> Result<ast::Expr, KclError> {
5728 let command = AstMutateCommand::AddVariableDeclaration {
5729 prefix: prefix.to_owned(),
5730 };
5731 let ret = match mutate_ast_node_by_source_ref(ast, source_ref, command) {
5732 Ok((_, ret)) => ret,
5733 Err(err) => {
5734 if let Some(var_name) = variable_name_containing_source_ref(ast, source_ref) {
5735 AstMutateCommandReturn::Name(var_name)
5736 } else {
5737 return Err(err);
5738 }
5739 }
5740 };
5741 let AstMutateCommandReturn::Name(var_name) = ret else {
5742 return Err(KclError::refactor(
5743 "Expected variable name returned from AddVariableDeclaration".to_owned(),
5744 ));
5745 };
5746 let var_expr = ast::Expr::Name(BoxNode::new(ast::Name::new(&var_name)));
5747 let Some(property) = property else {
5748 return Ok(var_expr);
5750 };
5751
5752 Ok(create_member_expression(var_expr, property))
5753}
5754
5755fn variable_name_containing_source_ref(ast: &ast::Node<ast::Program>, source_ref: &SourceRef) -> Option<String> {
5756 let source_range = match source_ref {
5757 SourceRef::Simple { range, .. } => *range,
5758 SourceRef::BackTrace { ranges } => {
5759 let [range] = ranges.as_slice() else {
5760 return None;
5761 };
5762 range.0
5763 }
5764 };
5765 ast.body.iter().find_map(|item| {
5766 let ast::BodyItem::VariableDeclaration(var_decl) = item else {
5767 return None;
5768 };
5769 let init_range = SourceRange::from(&var_decl.declaration.init);
5770 let source_is_inside_init = init_range.module_id() == source_range.module_id()
5771 && init_range.start() <= source_range.start()
5772 && source_range.end() <= init_range.end();
5773 if matches!(&var_decl.declaration.init, ast::Expr::SketchBlock(_))
5774 && init_range != source_range
5775 && source_is_inside_init
5776 {
5777 return None;
5778 }
5779 source_is_inside_init.then(|| var_decl.name().to_owned())
5780 })
5781}
5782
5783fn mutate_ast_node_by_source_ref(
5784 ast: &mut ast::Node<ast::Program>,
5785 source_ref: &SourceRef,
5786 command: AstMutateCommand,
5787) -> Result<(AstNodeRef, AstMutateCommandReturn), KclError> {
5788 let (source_range, node_path) = match source_ref {
5789 SourceRef::Simple { range, node_path } => (*range, node_path.clone()),
5790 SourceRef::BackTrace { ranges } => {
5791 let [range] = ranges.as_slice() else {
5792 return Err(KclError::refactor(format!(
5793 "Expected single source ref, got {}; ranges={ranges:#?}",
5794 ranges.len(),
5795 )));
5796 };
5797 (range.0, range.1.clone())
5798 }
5799 };
5800 let mut context = AstMutateContext {
5801 source_range,
5802 node_path,
5803 command,
5804 defined_names_stack: Default::default(),
5805 };
5806 let control = dfs_mut(ast, &mut context);
5807 match control {
5808 ControlFlow::Continue(_) => Err(KclError::refactor(
5809 "Could not find the KCL source for this edit. Try reloading the app, or update from code.".to_owned(),
5810 )),
5811 ControlFlow::Break(break_value) => break_value,
5812 }
5813}
5814
5815#[derive(Debug)]
5816struct AstMutateContext {
5817 source_range: SourceRange,
5818 node_path: Option<ast::NodePath>,
5819 command: AstMutateCommand,
5820 defined_names_stack: Vec<HashSet<String>>,
5821}
5822
5823#[derive(Debug)]
5824#[allow(clippy::large_enum_variant)]
5825enum AstMutateCommand {
5826 AddSketchBlockExprStmt {
5828 expr: ast::Expr,
5829 },
5830 AddSketchBlockVarDecl {
5832 prefix: String,
5833 expr: ast::Expr,
5834 },
5835 AddVariableDeclaration {
5836 prefix: String,
5837 },
5838 EditPoint {
5839 at: ast::Expr,
5840 },
5841 EditLine {
5842 start: ast::Expr,
5843 end: ast::Expr,
5844 construction: Option<bool>,
5845 },
5846 EditArc {
5847 start: ast::Expr,
5848 end: ast::Expr,
5849 center: ast::Expr,
5850 direction: Option<ArcDirection>,
5851 construction: Option<bool>,
5852 },
5853 EditCircle {
5854 start: ast::Expr,
5855 center: ast::Expr,
5856 construction: Option<bool>,
5857 },
5858 EditControlPointSpline {
5859 points: ast::Expr,
5860 construction: Option<bool>,
5861 },
5862 EditConstraintValue {
5863 value: ast::BinaryPart,
5864 },
5865 EditAngleConstraint {
5866 call: ast::BinaryPart,
5867 value: ast::BinaryPart,
5868 },
5869 EditDistanceConstraint {
5870 call: ast::BinaryPart,
5871 value: ast::BinaryPart,
5872 },
5873 EditDistanceConstraintLabelPosition {
5874 label_position: ast::Expr,
5875 },
5876 EditCallUnlabeled {
5877 arg: ast::Expr,
5878 },
5879 EditVarInitialValue {
5880 value: Number,
5881 },
5882 DeleteNode,
5883}
5884
5885impl AstMutateCommand {
5886 fn needs_defined_names_stack(&self) -> bool {
5887 matches!(
5888 self,
5889 AstMutateCommand::AddSketchBlockVarDecl { .. } | AstMutateCommand::AddVariableDeclaration { .. }
5890 )
5891 }
5892}
5893
5894#[derive(Debug)]
5895enum AstMutateCommandReturn {
5896 None,
5897 Name(String),
5898}
5899
5900#[derive(Debug, Clone)]
5901struct AstNodeRef {
5902 range: SourceRange,
5903 node_path: Option<ast::NodePath>,
5904}
5905
5906impl<T> From<&ast::Node<T>> for AstNodeRef {
5907 fn from(value: &ast::Node<T>) -> Self {
5908 AstNodeRef {
5909 range: value.into(),
5910 node_path: value.node_path.clone(),
5911 }
5912 }
5913}
5914
5915impl From<&ast::BodyItem> for AstNodeRef {
5916 fn from(value: &ast::BodyItem) -> Self {
5917 match value {
5918 ast::BodyItem::ImportStatement(node) => AstNodeRef {
5919 range: node.into(),
5920 node_path: node.node_path.clone(),
5921 },
5922 ast::BodyItem::ExpressionStatement(node) => AstNodeRef {
5923 range: node.into(),
5924 node_path: node.node_path.clone(),
5925 },
5926 ast::BodyItem::VariableDeclaration(node) => AstNodeRef {
5927 range: node.into(),
5928 node_path: node.node_path.clone(),
5929 },
5930 ast::BodyItem::TypeDeclaration(node) => AstNodeRef {
5931 range: node.into(),
5932 node_path: node.node_path.clone(),
5933 },
5934 ast::BodyItem::ReturnStatement(node) => AstNodeRef {
5935 range: node.into(),
5936 node_path: node.node_path.clone(),
5937 },
5938 }
5939 }
5940}
5941
5942impl From<&ast::Expr> for AstNodeRef {
5943 fn from(value: &ast::Expr) -> Self {
5944 AstNodeRef {
5945 range: SourceRange::from(value),
5946 node_path: value.node_path().cloned(),
5947 }
5948 }
5949}
5950
5951impl From<&AstMutateContext> for AstNodeRef {
5952 fn from(value: &AstMutateContext) -> Self {
5953 AstNodeRef {
5954 range: value.source_range,
5955 node_path: value.node_path.clone(),
5956 }
5957 }
5958}
5959
5960impl TryFrom<&NodeMut<'_>> for AstNodeRef {
5961 type Error = crate::walk::AstNodeError;
5962
5963 fn try_from(value: &NodeMut<'_>) -> Result<Self, Self::Error> {
5964 Ok(AstNodeRef {
5965 range: SourceRange::try_from(value)?,
5966 node_path: value.try_into()?,
5967 })
5968 }
5969}
5970
5971impl From<AstNodeRef> for SourceRange {
5972 fn from(value: AstNodeRef) -> Self {
5973 value.range
5974 }
5975}
5976
5977impl Visitor for AstMutateContext {
5978 type Break = Result<(AstNodeRef, AstMutateCommandReturn), KclError>;
5979 type Continue = ();
5980
5981 fn visit(&mut self, node: NodeMut<'_>) -> TraversalReturn<Self::Break, Self::Continue> {
5982 filter_and_process(self, node)
5983 }
5984
5985 fn finish(&mut self, node: NodeMut<'_>) {
5986 match &node {
5987 NodeMut::Program(_) | NodeMut::SketchBlock(_) => {
5988 self.defined_names_stack.pop();
5989 }
5990 _ => {}
5991 }
5992 }
5993}
5994
5995fn filter_and_process(
5996 ctx: &mut AstMutateContext,
5997 node: NodeMut,
5998) -> TraversalReturn<Result<(AstNodeRef, AstMutateCommandReturn), KclError>> {
5999 let Ok(node_range) = SourceRange::try_from(&node) else {
6000 return TraversalReturn::new_continue(());
6002 };
6003 if let NodeMut::VariableDeclaration(var_decl) = &node {
6008 let expr_range = SourceRange::from(&var_decl.declaration.init);
6009 let expr_node_path = var_decl.declaration.init.node_path();
6010 if source_ref_matches(ctx, expr_range, expr_node_path) {
6011 if let AstMutateCommand::AddVariableDeclaration { .. } = &ctx.command {
6012 return TraversalReturn::new_break(Ok((
6015 AstNodeRef::from(&**var_decl),
6016 AstMutateCommandReturn::Name(var_decl.name().to_owned()),
6017 )));
6018 }
6019 if let AstMutateCommand::DeleteNode = &ctx.command {
6020 return TraversalReturn {
6023 mutate_body_item: MutateBodyItem::Delete,
6024 control_flow: ControlFlow::Break(Ok((AstNodeRef::from(&*ctx), AstMutateCommandReturn::None))),
6025 };
6026 }
6027 }
6028 }
6029 if let NodeMut::ExpressionStatement(expr_stmt) = &node {
6032 let expr_range = SourceRange::from(&expr_stmt.expression);
6033 let expr_node_path = expr_stmt.expression.node_path();
6034 if source_ref_matches(ctx, expr_range, expr_node_path) {
6035 if let AstMutateCommand::AddVariableDeclaration { .. } = &ctx.command {
6036 let Ok(node_ref) = AstNodeRef::try_from(&node) else {
6039 return TraversalReturn::new_continue(());
6040 };
6041 return process(ctx, node).map_break(|result| result.map(|cmd_return| (node_ref, cmd_return)));
6042 }
6043 if let AstMutateCommand::DeleteNode = &ctx.command {
6044 return TraversalReturn {
6047 mutate_body_item: MutateBodyItem::Delete,
6048 control_flow: ControlFlow::Break(Ok((AstNodeRef::from(&*ctx), AstMutateCommandReturn::None))),
6049 };
6050 }
6051 }
6052 }
6053
6054 if ctx.command.needs_defined_names_stack() {
6055 if let NodeMut::Program(program) = &node {
6056 ctx.defined_names_stack.push(find_defined_names(*program));
6057 } else if let NodeMut::SketchBlock(block) = &node {
6058 ctx.defined_names_stack.push(find_defined_names(&block.body));
6059 }
6060 }
6061
6062 let node_path = <Option<ast::NodePath>>::try_from(&node).ok().flatten();
6064 if !source_ref_matches(ctx, node_range, node_path.as_ref()) {
6065 return TraversalReturn::new_continue(());
6066 }
6067 let Ok(node_ref) = AstNodeRef::try_from(&node) else {
6068 return TraversalReturn::new_continue(());
6069 };
6070 process(ctx, node).map_break(|result| result.map(|cmd_return| (node_ref, cmd_return)))
6071}
6072
6073fn source_ref_matches(ctx: &AstMutateContext, node_range: SourceRange, node_path: Option<&ast::NodePath>) -> bool {
6074 match &ctx.node_path {
6075 Some(target) => Some(target) == node_path,
6076 None => node_range == ctx.source_range,
6077 }
6078}
6079
6080fn is_angle_constraint_call_name(name: &str) -> bool {
6081 matches!(name, ANGLE_FN | ANGLE_DIMENSION_FN)
6082}
6083
6084fn is_distance_constraint_call_name(name: &str) -> bool {
6085 matches!(name, DISTANCE_FN | HORIZONTAL_DISTANCE_FN | VERTICAL_DISTANCE_FN)
6086}
6087
6088fn is_constraint_call_name(name: &str) -> bool {
6089 matches!(
6090 name,
6091 DISTANCE_FN
6092 | HORIZONTAL_DISTANCE_FN
6093 | VERTICAL_DISTANCE_FN
6094 | RADIUS_FN
6095 | DIAMETER_FN
6096 | ANGLE_FN
6097 | ANGLE_DIMENSION_FN
6098 )
6099}
6100
6101fn constraint_supports_label_position(part: &mut ast::BinaryPart) -> Option<&mut BoxNode<CallExpressionKw>> {
6102 if let ast::BinaryPart::CallExpressionKw(call) = part
6103 && is_constraint_call_name(call.callee.name.name.as_str())
6104 {
6105 Some(call)
6106 } else {
6107 None
6108 }
6109}
6110
6111fn process(ctx: &AstMutateContext, node: NodeMut) -> TraversalReturn<Result<AstMutateCommandReturn, KclError>> {
6112 match &ctx.command {
6113 AstMutateCommand::AddSketchBlockExprStmt { expr } => {
6114 if let NodeMut::SketchBlock(sketch_block) = node {
6115 sketch_block
6116 .body
6117 .items
6118 .push(ast::BodyItem::ExpressionStatement(ast::Node {
6119 inner: ast::ExpressionStatement {
6120 expression: expr.clone(),
6121 digest: None,
6122 },
6123 start: Default::default(),
6124 end: Default::default(),
6125 module_id: Default::default(),
6126 node_path: None,
6127 outer_attrs: Default::default(),
6128 pre_comments: Default::default(),
6129 comment_start: Default::default(),
6130 }));
6131 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6132 }
6133 }
6134 AstMutateCommand::AddSketchBlockVarDecl { prefix, expr } => {
6135 if let NodeMut::SketchBlock(sketch_block) = node {
6136 let empty_defined_names = HashSet::new();
6137 let defined_names = ctx.defined_names_stack.last().unwrap_or(&empty_defined_names);
6138 let Ok(name) = next_free_name(prefix, defined_names) else {
6139 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6140 };
6141 sketch_block
6142 .body
6143 .items
6144 .push(ast::BodyItem::VariableDeclaration(BoxNode::new(ast::Node::no_src(
6145 ast::VariableDeclaration::new(
6146 ast::VariableDeclarator::new(&name, expr.clone()),
6147 ast::ItemVisibility::Default,
6148 ast::VariableKind::Const,
6149 ),
6150 ))));
6151 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::Name(name)));
6152 }
6153 }
6154 AstMutateCommand::AddVariableDeclaration { prefix } => {
6155 if let NodeMut::VariableDeclaration(inner) = node {
6156 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::Name(inner.name().to_owned())));
6157 }
6158 if let NodeMut::ExpressionStatement(expr_stmt) = node {
6159 let empty_defined_names = HashSet::new();
6160 let defined_names = ctx.defined_names_stack.last().unwrap_or(&empty_defined_names);
6161 let Ok(name) = next_free_name(prefix, defined_names) else {
6162 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6164 };
6165 let mutate_node =
6166 ast::BodyItem::VariableDeclaration(BoxNode::new(ast::Node::no_src(ast::VariableDeclaration::new(
6167 ast::VariableDeclarator::new(&name, expr_stmt.expression.clone()),
6168 ast::ItemVisibility::Default,
6169 ast::VariableKind::Const,
6170 ))));
6171 return TraversalReturn {
6172 mutate_body_item: MutateBodyItem::Mutate(Box::new(mutate_node)),
6173 control_flow: ControlFlow::Break(Ok(AstMutateCommandReturn::Name(name))),
6174 };
6175 }
6176 }
6177 AstMutateCommand::EditPoint { at } => {
6178 if let NodeMut::CallExpressionKw(call) = node {
6179 if call.callee.name.name != POINT_FN {
6180 return TraversalReturn::new_continue(());
6181 }
6182 for labeled_arg in &mut call.arguments {
6184 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(POINT_AT_PARAM) {
6185 labeled_arg.arg = at.clone();
6186 }
6187 }
6188 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6189 }
6190 }
6191 AstMutateCommand::EditLine {
6192 start,
6193 end,
6194 construction,
6195 } => {
6196 if let NodeMut::CallExpressionKw(call) = node {
6197 if call.callee.name.name != LINE_FN {
6198 return TraversalReturn::new_continue(());
6199 }
6200 for labeled_arg in &mut call.arguments {
6202 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(LINE_START_PARAM) {
6203 labeled_arg.arg = start.clone();
6204 }
6205 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(LINE_END_PARAM) {
6206 labeled_arg.arg = end.clone();
6207 }
6208 }
6209 if let Some(construction_value) = construction {
6211 let construction_exists = call
6212 .arguments
6213 .iter()
6214 .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
6215 if *construction_value {
6216 if construction_exists {
6218 for labeled_arg in &mut call.arguments {
6220 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
6221 labeled_arg.arg =
6222 ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6223 value: ast::LiteralValue::Bool(true),
6224 raw: "true".to_string(),
6225 digest: None,
6226 })));
6227 }
6228 }
6229 } else {
6230 call.arguments.push(ast::LabeledArg {
6232 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
6233 arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6234 value: ast::LiteralValue::Bool(true),
6235 raw: "true".to_string(),
6236 digest: None,
6237 }))),
6238 });
6239 }
6240 } else {
6241 call.arguments
6243 .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
6244 }
6245 }
6246 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6247 }
6248 }
6249 AstMutateCommand::EditArc {
6250 start,
6251 end,
6252 center,
6253 direction,
6254 construction,
6255 } => {
6256 if let NodeMut::CallExpressionKw(call) = node {
6257 if call.callee.name.name != ARC_FN {
6258 return TraversalReturn::new_continue(());
6259 }
6260 for labeled_arg in &mut call.arguments {
6262 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_START_PARAM) {
6263 labeled_arg.arg = start.clone();
6264 }
6265 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_END_PARAM) {
6266 labeled_arg.arg = end.clone();
6267 }
6268 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_CENTER_PARAM) {
6269 labeled_arg.arg = center.clone();
6270 }
6271 }
6272 if let Some(direction_value) = direction {
6274 let direction_exists = call
6275 .arguments
6276 .iter()
6277 .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_DIRECTION_PARAM));
6278 if direction_value.is_clockwise() {
6279 let direction_ast = ast::Expr::Name(BoxNode::new(ast::Name::new(ARC_DIRECTION_CW_NAME)));
6280 if direction_exists {
6281 for labeled_arg in &mut call.arguments {
6283 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_DIRECTION_PARAM) {
6284 labeled_arg.arg = direction_ast.clone();
6285 }
6286 }
6287 } else {
6288 call.arguments.push(ast::LabeledArg {
6290 label: Some(ast::Identifier::new(ARC_DIRECTION_PARAM)),
6291 arg: direction_ast,
6292 });
6293 }
6294 } else {
6295 call.arguments
6298 .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(ARC_DIRECTION_PARAM));
6299 }
6300 }
6301 if let Some(construction_value) = construction {
6303 let construction_exists = call
6304 .arguments
6305 .iter()
6306 .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
6307 if *construction_value {
6308 if construction_exists {
6310 for labeled_arg in &mut call.arguments {
6312 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
6313 labeled_arg.arg =
6314 ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6315 value: ast::LiteralValue::Bool(true),
6316 raw: "true".to_string(),
6317 digest: None,
6318 })));
6319 }
6320 }
6321 } else {
6322 call.arguments.push(ast::LabeledArg {
6324 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
6325 arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6326 value: ast::LiteralValue::Bool(true),
6327 raw: "true".to_string(),
6328 digest: None,
6329 }))),
6330 });
6331 }
6332 } else {
6333 call.arguments
6335 .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
6336 }
6337 }
6338 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6339 }
6340 }
6341 AstMutateCommand::EditCircle {
6342 start,
6343 center,
6344 construction,
6345 } => {
6346 if let NodeMut::CallExpressionKw(call) = node {
6347 if call.callee.name.name != CIRCLE_FN {
6348 return TraversalReturn::new_continue(());
6349 }
6350 for labeled_arg in &mut call.arguments {
6352 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CIRCLE_START_PARAM) {
6353 labeled_arg.arg = start.clone();
6354 }
6355 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CIRCLE_CENTER_PARAM) {
6356 labeled_arg.arg = center.clone();
6357 }
6358 }
6359 if let Some(construction_value) = construction {
6361 let construction_exists = call
6362 .arguments
6363 .iter()
6364 .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
6365 if *construction_value {
6366 if construction_exists {
6367 for labeled_arg in &mut call.arguments {
6369 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
6370 labeled_arg.arg =
6371 ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6372 value: ast::LiteralValue::Bool(true),
6373 raw: "true".to_string(),
6374 digest: None,
6375 })));
6376 }
6377 }
6378 } else {
6379 call.arguments.push(ast::LabeledArg {
6381 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
6382 arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6383 value: ast::LiteralValue::Bool(true),
6384 raw: "true".to_string(),
6385 digest: None,
6386 }))),
6387 });
6388 }
6389 } else {
6390 call.arguments
6392 .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
6393 }
6394 }
6395 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6396 }
6397 }
6398 AstMutateCommand::EditControlPointSpline { points, construction } => {
6399 if let NodeMut::CallExpressionKw(call) = node {
6400 if call.callee.name.name != CONTROL_POINT_SPLINE_FN {
6401 return TraversalReturn::new_continue(());
6402 }
6403 for labeled_arg in &mut call.arguments {
6404 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONTROL_POINT_SPLINE_POINTS_PARAM)
6405 {
6406 labeled_arg.arg = points.clone();
6407 }
6408 }
6409 if let Some(construction_value) = construction {
6411 let construction_exists = call
6412 .arguments
6413 .iter()
6414 .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
6415 if *construction_value {
6416 if construction_exists {
6417 for labeled_arg in &mut call.arguments {
6418 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
6419 labeled_arg.arg =
6420 ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6421 value: ast::LiteralValue::Bool(true),
6422 raw: "true".to_string(),
6423 digest: None,
6424 })));
6425 }
6426 }
6427 } else {
6428 call.arguments.push(ast::LabeledArg {
6429 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
6430 arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6431 value: ast::LiteralValue::Bool(true),
6432 raw: "true".to_string(),
6433 digest: None,
6434 }))),
6435 });
6436 }
6437 } else {
6438 call.arguments
6439 .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
6440 }
6441 }
6442 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6443 }
6444 }
6445 AstMutateCommand::EditConstraintValue { value } => {
6446 if let NodeMut::BinaryExpression(binary_expr) = node {
6447 let left_is_constraint = matches!(
6448 &binary_expr.left,
6449 ast::BinaryPart::CallExpressionKw(call) if is_constraint_call_name(call.callee.name.name.as_str())
6450 );
6451 if left_is_constraint {
6452 binary_expr.right = value.clone();
6453 } else {
6454 binary_expr.left = value.clone();
6455 }
6456
6457 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6458 }
6459 }
6460 AstMutateCommand::EditAngleConstraint { call, value } => {
6461 if let NodeMut::BinaryExpression(binary_expr) = node {
6462 let left_is_angle = matches!(
6463 &binary_expr.left,
6464 ast::BinaryPart::CallExpressionKw(existing_call)
6465 if is_angle_constraint_call_name(existing_call.callee.name.name.as_str())
6466 );
6467 let right_is_angle = matches!(
6468 &binary_expr.right,
6469 ast::BinaryPart::CallExpressionKw(existing_call)
6470 if is_angle_constraint_call_name(existing_call.callee.name.name.as_str())
6471 );
6472
6473 match (left_is_angle, right_is_angle) {
6474 (true, _) => {
6475 binary_expr.left = call.clone();
6476 binary_expr.right = value.clone();
6477 }
6478 (false, true) => {
6479 binary_expr.left = value.clone();
6480 binary_expr.right = call.clone();
6481 }
6482 (false, false) => return TraversalReturn::new_continue(()),
6483 }
6484
6485 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6486 }
6487 }
6488 AstMutateCommand::EditDistanceConstraint { call, value } => {
6489 if let NodeMut::BinaryExpression(binary_expr) = node {
6490 let left_is_distance = matches!(
6491 &binary_expr.left,
6492 ast::BinaryPart::CallExpressionKw(existing_call)
6493 if is_distance_constraint_call_name(existing_call.callee.name.name.as_str())
6494 );
6495 let right_is_distance = matches!(
6496 &binary_expr.right,
6497 ast::BinaryPart::CallExpressionKw(existing_call)
6498 if is_distance_constraint_call_name(existing_call.callee.name.name.as_str())
6499 );
6500
6501 match (left_is_distance, right_is_distance) {
6502 (true, _) => {
6503 binary_expr.left = call.clone();
6504 binary_expr.right = value.clone();
6505 }
6506 (false, true) => {
6507 binary_expr.left = value.clone();
6508 binary_expr.right = call.clone();
6509 }
6510 (false, false) => return TraversalReturn::new_continue(()),
6511 }
6512
6513 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6514 }
6515 }
6516 AstMutateCommand::EditDistanceConstraintLabelPosition { label_position } => {
6517 if let NodeMut::BinaryExpression(binary_expr) = node {
6518 let call = if let Some(call) = constraint_supports_label_position(&mut binary_expr.left) {
6519 call
6520 } else if let Some(call) = constraint_supports_label_position(&mut binary_expr.right) {
6521 call
6522 } else {
6523 return TraversalReturn::new_continue(());
6524 };
6525
6526 if let Some(label_arg) = call
6527 .arguments
6528 .iter_mut()
6529 .find(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(LABEL_POSITION_PARAM))
6530 {
6531 label_arg.arg = label_position.clone();
6532 } else {
6533 call.arguments.push(ast::LabeledArg {
6534 label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
6535 arg: label_position.clone(),
6536 });
6537 }
6538
6539 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6540 }
6541 }
6542 AstMutateCommand::EditCallUnlabeled { arg } => {
6543 if let NodeMut::CallExpressionKw(call) = node {
6544 call.unlabeled = Some(arg.clone());
6545 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6546 }
6547 }
6548 AstMutateCommand::EditVarInitialValue { value } => {
6549 if let NodeMut::SketchVar(sketch_var) = node {
6553 let Ok(literal) = to_source_number(*value) else {
6554 return TraversalReturn::new_break(Err(KclError::refactor(format!(
6555 "Could not convert number to AST literal: {:?}",
6556 *value
6557 ))));
6558 };
6559 sketch_var.initial = Some(BoxNode::new(ast::Node::no_src(literal)));
6560 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6561 }
6562 }
6563 AstMutateCommand::DeleteNode => {
6564 return TraversalReturn {
6565 mutate_body_item: MutateBodyItem::Delete,
6566 control_flow: ControlFlow::Break(Ok(AstMutateCommandReturn::None)),
6567 };
6568 }
6569 }
6570 TraversalReturn::new_continue(())
6571}
6572
6573struct FindSketchBlockSourceRange {
6574 target_before_mutation: SourceRange,
6576 found: Cell<Option<AstNodeRef>>,
6580}
6581
6582impl<'a> crate::walk::Visitor<'a> for &FindSketchBlockSourceRange {
6583 type Error = crate::front::Error;
6584
6585 fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
6586 let Ok(node_range) = SourceRange::try_from(&node) else {
6587 return Ok(true);
6588 };
6589
6590 if let crate::walk::Node::SketchBlock(sketch_block) = node {
6591 if node_range.module_id() == self.target_before_mutation.module_id()
6592 && node_range.start() == self.target_before_mutation.start()
6593 && node_range.end() >= self.target_before_mutation.end()
6595 {
6596 self.found.set(sketch_block.body.items.last().map(|item| match item {
6597 ast::BodyItem::VariableDeclaration(node) => AstNodeRef::from(&node.declaration.init),
6601 _ => AstNodeRef::from(item),
6602 }));
6603 return Ok(false);
6604 } else {
6605 return Ok(true);
6608 }
6609 }
6610
6611 for child in node.children().iter() {
6612 if !child.visit(*self)? {
6613 return Ok(false);
6614 }
6615 }
6616
6617 Ok(true)
6618 }
6619}
6620
6621struct FindSketchBlockByNodePath {
6622 target_node_path: ast::NodePath,
6624 found: Cell<Option<AstNodeRef>>,
6628}
6629
6630impl<'a> crate::walk::Visitor<'a> for &FindSketchBlockByNodePath {
6631 type Error = crate::front::Error;
6632
6633 fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
6634 let Ok(node_path) = <Option<ast::NodePath>>::try_from(&node) else {
6635 return Ok(true);
6636 };
6637
6638 if let crate::walk::Node::SketchBlock(sketch_block) = node {
6639 if let Some(node_path) = node_path
6640 && node_path == self.target_node_path
6641 {
6642 self.found.set(sketch_block.body.items.last().map(|item| match item {
6643 ast::BodyItem::VariableDeclaration(node) => AstNodeRef::from(&node.declaration.init),
6647 _ => AstNodeRef::from(item),
6648 }));
6649
6650 return Ok(false);
6651 } else {
6652 return Ok(true);
6655 }
6656 }
6657
6658 for child in node.children().iter() {
6659 if !child.visit(*self)? {
6660 return Ok(false);
6661 }
6662 }
6663
6664 Ok(true)
6665 }
6666}
6667
6668fn find_sketch_block_added_item(
6676 ast: &ast::Node<ast::Program>,
6677 sketch_block_before_mutation: &AstNodeRef,
6678) -> Result<AstNodeRef, KclError> {
6679 if let Some(node_path) = &sketch_block_before_mutation.node_path {
6680 let find = FindSketchBlockByNodePath {
6681 target_node_path: node_path.clone(),
6682 found: Cell::new(None),
6683 };
6684 let node = crate::walk::Node::from(ast);
6685 node.visit(&find).map_err(|err| KclError::refactor(err.msg))?;
6686 find.found.into_inner().ok_or_else(|| {
6687 KclError::refactor(format!(
6688 "Node ID after mutation not found for Node ID before mutation: {node_path:?}"
6689 ))
6690 })
6691 } else {
6692 let find = FindSketchBlockSourceRange {
6694 target_before_mutation: sketch_block_before_mutation.range,
6695 found: Cell::new(None),
6696 };
6697 let node = crate::walk::Node::from(ast);
6698 node.visit(&find).map_err(|err| KclError::refactor(err.msg))?;
6699 find.found.into_inner().ok_or_else(|| KclError::refactor(
6700 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?"),
6701 ))
6702 }
6703}
6704
6705fn format_kcl_error_message(prefix: &str, error: &KclError) -> String {
6706 let message = error.message().trim();
6707 let message = if message.is_empty() {
6708 "unknown parse error"
6709 } else {
6710 message
6711 };
6712
6713 format!("{prefix}: {message}")
6714}
6715
6716fn parse_frontend_mutation_source(source: &str, parse_error_prefix: &str, no_ast_message: &str) -> ExecResult<Program> {
6717 let (program, errors) = Program::parse(source).map_err(|err| {
6718 KclErrorWithOutputs::no_outputs(KclError::refactor(format_kcl_error_message(parse_error_prefix, &err)))
6719 })?;
6720 if !errors.is_empty() {
6721 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
6722 format_compilation_issues(parse_error_prefix, &errors),
6723 )));
6724 }
6725
6726 program.ok_or_else(|| KclErrorWithOutputs::no_outputs(KclError::refactor(no_ast_message.to_owned())))
6727}
6728
6729fn format_compilation_issues(prefix: &str, issues: &[CompilationIssue]) -> String {
6730 let Some(first_issue) = issues
6731 .iter()
6732 .find(|issue| issue.severity.is_err())
6733 .or_else(|| issues.first())
6734 else {
6735 return prefix.to_owned();
6736 };
6737
6738 let message = first_issue.message.trim();
6739 let message = if message.is_empty() {
6740 "unknown parse error"
6741 } else {
6742 message
6743 };
6744
6745 if issues.len() > 1 {
6746 format!("{prefix}: {message} (+{} more)", issues.len() - 1)
6747 } else {
6748 format!("{prefix}: {message}")
6749 }
6750}
6751
6752fn source_from_ast(ast: &ast::Node<ast::Program>) -> String {
6753 ast.recast_top(&Default::default(), 0)
6755}
6756
6757struct FindNumericLiteral {
6758 target: SourceRange,
6759 found: Cell<Option<ast::NumericLiteral>>,
6760}
6761
6762impl<'a> crate::walk::Visitor<'a> for &FindNumericLiteral {
6763 type Error = crate::front::Error;
6764
6765 fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
6766 let Ok(node_range) = SourceRange::try_from(&node) else {
6767 return Ok(true);
6768 };
6769
6770 if node_range == self.target
6771 && let crate::walk::Node::NumericLiteral(literal) = node
6772 {
6773 self.found.set(Some(literal.inner.clone()));
6774 return Ok(false);
6775 }
6776
6777 for child in node.children().iter() {
6778 if !child.visit(*self)? {
6779 return Ok(false);
6780 }
6781 }
6782
6783 Ok(true)
6784 }
6785}
6786
6787fn numeric_literal_at_source_range(ast: &ast::Node<ast::Program>, target: SourceRange) -> Option<ast::NumericLiteral> {
6788 let find = FindNumericLiteral {
6789 target,
6790 found: Cell::new(None),
6791 };
6792 let node = crate::walk::Node::from(ast);
6793 node.visit(&find).ok()?;
6794 find.found.into_inner()
6795}
6796
6797struct FindSketchVarInitialByNodePath<'a> {
6798 target: &'a ast::NodePath,
6799 sketch_var_found: Cell<bool>,
6800 initial_literal: Cell<Option<ast::NumericLiteral>>,
6801}
6802
6803impl<'a, 'b> crate::walk::Visitor<'b> for &FindSketchVarInitialByNodePath<'a> {
6804 type Error = crate::front::Error;
6805
6806 fn visit_node(&self, node: crate::walk::Node<'b>) -> anyhow::Result<bool, Self::Error> {
6807 if let crate::walk::Node::SketchVar(sketch_var) = node
6808 && sketch_var.node_path.as_ref() == Some(self.target)
6809 {
6810 self.sketch_var_found.set(true);
6811 if let Some(initial) = &sketch_var.initial {
6812 self.initial_literal.set(Some(initial.inner.clone()));
6813 }
6814 return Ok(false);
6815 }
6816
6817 for child in node.children().iter() {
6818 if !child.visit(*self)? {
6819 return Ok(false);
6820 }
6821 }
6822
6823 Ok(true)
6824 }
6825}
6826
6827fn numeric_literal_at_node_path(
6837 ast: &ast::Node<ast::Program>,
6838 node_path: Option<&ast::NodePath>,
6839 source_range: SourceRange,
6840) -> Option<Option<ast::NumericLiteral>> {
6841 let Some(node_path) = node_path else {
6842 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";
6843 #[cfg(target_arch = "wasm32")]
6844 web_sys::console::warn_1(&message.into());
6845 #[cfg(not(target_arch = "wasm32"))]
6846 eprintln!("WARNING: {message}");
6847 return numeric_literal_at_source_range(ast, source_range).map(Some);
6848 };
6849 let find = FindSketchVarInitialByNodePath {
6850 target: node_path,
6851 sketch_var_found: Cell::new(false),
6852 initial_literal: Cell::new(None),
6853 };
6854 let node = crate::walk::Node::from(ast);
6855 node.visit(&find).ok()?;
6856 if !find.sketch_var_found.get() {
6857 return None;
6858 }
6859 Some(find.initial_literal.into_inner())
6860}
6861
6862fn suffix_length_unit(suffix: NumericSuffix) -> Option<UnitLength> {
6863 match suffix {
6864 NumericSuffix::Mm => Some(UnitLength::Millimeters),
6865 NumericSuffix::Cm => Some(UnitLength::Centimeters),
6866 NumericSuffix::M => Some(UnitLength::Meters),
6867 NumericSuffix::Inch => Some(UnitLength::Inches),
6868 NumericSuffix::Ft => Some(UnitLength::Feet),
6869 NumericSuffix::Yd => Some(UnitLength::Yards),
6870 _ => None,
6871 }
6872}
6873
6874fn number_value_in_default_length_units(number: Number, default_length_unit: UnitLength) -> f64 {
6875 match suffix_length_unit(number.units) {
6876 Some(unit) => adjust_length(unit, number.value, default_length_unit).0,
6877 None => number.value,
6878 }
6879}
6880
6881fn literal_value_in_default_length_units(literal: &ast::NumericLiteral, default_length_unit: UnitLength) -> f64 {
6882 match suffix_length_unit(literal.suffix) {
6883 Some(unit) => adjust_length(unit, literal.value, default_length_unit).0,
6884 None => literal.value,
6885 }
6886}
6887
6888fn var_solution_needs_commit(
6889 current_literal: &ast::NumericLiteral,
6890 solved_value: Number,
6891 default_length_unit: UnitLength,
6892) -> bool {
6893 let current = literal_value_in_default_length_units(current_literal, default_length_unit);
6894 let solved = number_value_in_default_length_units(solved_value, default_length_unit);
6895
6896 (current - solved).abs() > 1e-9
6897}
6898
6899fn preserve_var_solution_literal_style(
6900 current_literal: &ast::NumericLiteral,
6901 solved_value: Number,
6902 default_length_unit: UnitLength,
6903) -> Number {
6904 if current_literal.suffix == NumericSuffix::None {
6905 return Number {
6906 value: number_value_in_default_length_units(solved_value, default_length_unit),
6907 units: NumericSuffix::None,
6908 };
6909 }
6910
6911 let Some(current_unit) = suffix_length_unit(current_literal.suffix) else {
6912 return solved_value;
6913 };
6914
6915 let solved_default_value = number_value_in_default_length_units(solved_value, default_length_unit);
6916 Number {
6917 value: adjust_length(default_length_unit, solved_default_value, current_unit).0,
6918 units: current_literal.suffix,
6919 }
6920}
6921
6922pub(crate) fn to_ast_point2d(point: &Point2d<Expr>) -> anyhow::Result<ast::Expr> {
6923 Ok(ast::Expr::ArrayExpression(BoxNode::new(ast::Node {
6924 inner: ast::ArrayExpression {
6925 elements: vec![to_source_expr(&point.x)?, to_source_expr(&point.y)?],
6926 non_code_meta: Default::default(),
6927 digest: None,
6928 },
6929 start: Default::default(),
6930 end: Default::default(),
6931 module_id: Default::default(),
6932 node_path: None,
6933 outer_attrs: Default::default(),
6934 pre_comments: Default::default(),
6935 comment_start: Default::default(),
6936 })))
6937}
6938
6939pub(crate) fn to_ast_point2d_array(points: &[Point2d<Expr>]) -> anyhow::Result<ast::Expr> {
6940 Ok(ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(
6941 ast::ArrayExpression {
6942 elements: points.iter().map(to_ast_point2d).collect::<anyhow::Result<Vec<_>>>()?,
6943 digest: None,
6944 non_code_meta: Default::default(),
6945 },
6946 ))))
6947}
6948
6949fn to_ast_point2d_number(point: &Point2d<Number>) -> anyhow::Result<ast::Expr> {
6950 Ok(ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(
6951 ast::ArrayExpression {
6952 elements: vec![
6953 ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal::from(to_source_number(
6954 point.x,
6955 )?)))),
6956 ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal::from(to_source_number(
6957 point.y,
6958 )?)))),
6959 ],
6960 non_code_meta: Default::default(),
6961 digest: None,
6962 },
6963 ))))
6964}
6965
6966fn to_source_expr(expr: &Expr) -> anyhow::Result<ast::Expr> {
6967 match expr {
6968 Expr::Number(number) => Ok(ast::Expr::Literal(BoxNode::new(ast::Node {
6969 inner: ast::Literal::from(to_source_number(*number)?),
6970 start: Default::default(),
6971 end: Default::default(),
6972 module_id: Default::default(),
6973 node_path: None,
6974 outer_attrs: Default::default(),
6975 pre_comments: Default::default(),
6976 comment_start: Default::default(),
6977 }))),
6978 Expr::Var(number) => Ok(ast::Expr::SketchVar(BoxNode::new(ast::Node {
6979 inner: ast::SketchVar {
6980 initial: Some(BoxNode::new(ast::Node {
6981 inner: to_source_number(*number)?,
6982 start: Default::default(),
6983 end: Default::default(),
6984 module_id: Default::default(),
6985 node_path: None,
6986 outer_attrs: Default::default(),
6987 pre_comments: Default::default(),
6988 comment_start: Default::default(),
6989 })),
6990 digest: None,
6991 },
6992 start: Default::default(),
6993 end: Default::default(),
6994 module_id: Default::default(),
6995 node_path: None,
6996 outer_attrs: Default::default(),
6997 pre_comments: Default::default(),
6998 comment_start: Default::default(),
6999 }))),
7000 Expr::Variable(variable) => Ok(ast_name_expr(variable.clone())),
7001 }
7002}
7003
7004fn to_source_number(number: Number) -> anyhow::Result<ast::NumericLiteral> {
7005 Ok(ast::NumericLiteral {
7006 value: number.value,
7007 suffix: number.units,
7008 raw: format_number_literal(number.value, number.units, None)?,
7009 digest: None,
7010 })
7011}
7012
7013pub(crate) fn ast_name_expr(name: String) -> ast::Expr {
7014 ast::Expr::Name(BoxNode::new(ast_name(name)))
7015}
7016
7017fn ast_name(name: String) -> ast::Node<ast::Name> {
7018 ast::Node {
7019 inner: ast::Name {
7020 name: ast::Node {
7021 inner: ast::Identifier { name, digest: None },
7022 start: Default::default(),
7023 end: Default::default(),
7024 module_id: Default::default(),
7025 node_path: None,
7026 outer_attrs: Default::default(),
7027 pre_comments: Default::default(),
7028 comment_start: Default::default(),
7029 },
7030 path: Vec::new(),
7031 abs_path: false,
7032 digest: None,
7033 },
7034 start: Default::default(),
7035 end: Default::default(),
7036 module_id: Default::default(),
7037 node_path: None,
7038 outer_attrs: Default::default(),
7039 pre_comments: Default::default(),
7040 comment_start: Default::default(),
7041 }
7042}
7043
7044pub(crate) fn ast_sketch2_name(name: &str) -> ast::Name {
7045 ast::Name {
7046 name: ast::Node {
7047 inner: ast::Identifier {
7048 name: name.to_owned(),
7049 digest: None,
7050 },
7051 start: Default::default(),
7052 end: Default::default(),
7053 module_id: Default::default(),
7054 node_path: None,
7055 outer_attrs: Default::default(),
7056 pre_comments: Default::default(),
7057 comment_start: Default::default(),
7058 },
7059 path: Default::default(),
7060 abs_path: false,
7061 digest: None,
7062 }
7063}
7064
7065pub(crate) fn create_coincident_ast(exprs: impl IntoIterator<Item = ast::Expr>) -> ast::Expr {
7067 let elements = exprs.into_iter().collect::<Vec<_>>();
7068 debug_assert!(elements.len() >= 2, "Coincident AST should have at least 2 inputs");
7069
7070 let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7072 elements,
7073 digest: None,
7074 non_code_meta: Default::default(),
7075 })));
7076
7077 ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7079 callee: ast::Node::no_src(ast_sketch2_name(COINCIDENT_FN)),
7080 unlabeled: Some(array_expr),
7081 arguments: Default::default(),
7082 digest: None,
7083 non_code_meta: Default::default(),
7084 })))
7085}
7086
7087pub(crate) fn create_horizontal_ast(line_expr: ast::Expr) -> ast::Expr {
7089 ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7090 callee: ast::Node::no_src(ast_sketch2_name(HORIZONTAL_FN)),
7091 unlabeled: Some(line_expr),
7092 arguments: Default::default(),
7093 digest: None,
7094 non_code_meta: Default::default(),
7095 })))
7096}
7097
7098pub(crate) fn create_vertical_ast(line_expr: ast::Expr) -> ast::Expr {
7100 ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7101 callee: ast::Node::no_src(ast_sketch2_name(VERTICAL_FN)),
7102 unlabeled: Some(line_expr),
7103 arguments: Default::default(),
7104 digest: None,
7105 non_code_meta: Default::default(),
7106 })))
7107}
7108
7109pub(crate) fn create_member_expression(object_expr: ast::Expr, property: &str) -> ast::Expr {
7111 ast::Expr::MemberExpression(BoxNode::new(ast::Node::no_src(ast::MemberExpression {
7112 object: object_expr,
7113 property: ast::Expr::Name(BoxNode::new(ast::Node::no_src(ast::Name {
7114 name: ast::Node::no_src(ast::Identifier {
7115 name: property.to_string(),
7116 digest: None,
7117 }),
7118 path: Vec::new(),
7119 abs_path: false,
7120 digest: None,
7121 }))),
7122 computed: false,
7123 digest: None,
7124 })))
7125}
7126
7127pub(crate) fn create_index_expression(object_expr: ast::Expr, index: usize) -> ast::Expr {
7128 ast::Expr::MemberExpression(BoxNode::new(ast::Node::no_src(ast::MemberExpression {
7129 object: object_expr,
7130 property: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal::from(
7131 ast::NumericLiteral {
7132 value: index as f64,
7133 suffix: NumericSuffix::None,
7134 raw: index.to_string(),
7135 digest: None,
7136 },
7137 )))),
7138 computed: true,
7139 digest: None,
7140 })))
7141}
7142
7143fn create_fixed_point_constraint_ast(point_expr: ast::Expr, position: Point2d<Number>) -> anyhow::Result<ast::Expr> {
7145 let x_literal = ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal::from(to_source_number(
7147 position.x,
7148 )?))));
7149 let y_literal = ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal::from(to_source_number(
7150 position.y,
7151 )?))));
7152 let point_array = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7153 elements: vec![x_literal, y_literal],
7154 digest: None,
7155 non_code_meta: Default::default(),
7156 })));
7157
7158 let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7160 elements: vec![point_expr, point_array],
7161 digest: None,
7162 non_code_meta: Default::default(),
7163 })));
7164
7165 Ok(ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(
7167 ast::CallExpressionKw {
7168 callee: ast::Node::no_src(ast_sketch2_name(FIXED_FN)),
7169 unlabeled: Some(array_expr),
7170 arguments: Default::default(),
7171 digest: None,
7172 non_code_meta: Default::default(),
7173 },
7174 ))))
7175}
7176
7177pub(crate) fn create_equal_length_ast(line_exprs: Vec<ast::Expr>) -> ast::Expr {
7179 let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7180 elements: line_exprs,
7181 digest: None,
7182 non_code_meta: Default::default(),
7183 })));
7184
7185 ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7187 callee: ast::Node::no_src(ast_sketch2_name(EQUAL_LENGTH_FN)),
7188 unlabeled: Some(array_expr),
7189 arguments: Default::default(),
7190 digest: None,
7191 non_code_meta: Default::default(),
7192 })))
7193}
7194
7195pub(crate) fn create_equal_radius_ast(segment_exprs: Vec<ast::Expr>) -> ast::Expr {
7197 let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7198 elements: segment_exprs,
7199 digest: None,
7200 non_code_meta: Default::default(),
7201 })));
7202
7203 ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7204 callee: ast::Node::no_src(ast_sketch2_name(EQUAL_RADIUS_FN)),
7205 unlabeled: Some(array_expr),
7206 arguments: Default::default(),
7207 digest: None,
7208 non_code_meta: Default::default(),
7209 })))
7210}
7211
7212pub(crate) fn create_tangent_ast(seg1_expr: ast::Expr, seg2_expr: ast::Expr) -> ast::Expr {
7214 let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7215 elements: vec![seg1_expr, seg2_expr],
7216 digest: None,
7217 non_code_meta: Default::default(),
7218 })));
7219
7220 ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7221 callee: ast::Node::no_src(ast_sketch2_name(TANGENT_FN)),
7222 unlabeled: Some(array_expr),
7223 arguments: Default::default(),
7224 digest: None,
7225 non_code_meta: Default::default(),
7226 })))
7227}
7228
7229pub(crate) fn create_symmetric_ast(input_exprs: Vec<ast::Expr>, axis_expr: ast::Expr) -> ast::Expr {
7231 let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7232 elements: input_exprs,
7233 digest: None,
7234 non_code_meta: Default::default(),
7235 })));
7236 let arguments = vec![ast::LabeledArg {
7237 label: Some(ast::Identifier::new(SYMMETRIC_AXIS_PARAM)),
7238 arg: axis_expr,
7239 }];
7240
7241 ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7242 callee: ast::Node::no_src(ast_sketch2_name(SYMMETRIC_FN)),
7243 unlabeled: Some(array_expr),
7244 arguments,
7245 digest: None,
7246 non_code_meta: Default::default(),
7247 })))
7248}
7249
7250pub(crate) fn create_midpoint_ast(segment_expr: ast::Expr, point_expr: ast::Expr) -> ast::Expr {
7252 let arguments = vec![ast::LabeledArg {
7253 label: Some(ast::Identifier::new(MIDPOINT_POINT_PARAM)),
7254 arg: point_expr,
7255 }];
7256
7257 ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7258 callee: ast::Node::no_src(ast_sketch2_name(MIDPOINT_FN)),
7259 unlabeled: Some(segment_expr),
7260 arguments,
7261 digest: None,
7262 non_code_meta: Default::default(),
7263 })))
7264}
7265
7266fn issue_source_range(error: &KclError) -> SourceRange {
7271 let source_ranges = error.source_ranges();
7272 source_ranges
7273 .iter()
7274 .find(|range| range.is_top_level_module())
7275 .or_else(|| source_ranges.first())
7276 .copied()
7277 .unwrap_or_else(SourceRange::synthetic)
7278}
7279
7280#[cfg(test)]
7281mod tests {
7282 use std::sync;
7283
7284 use kcl_api::Distance;
7285 use kcl_api::Fixed;
7286 use kcl_api::FixedPoint;
7287 use kcl_api::Midpoint;
7288 use kcl_api::NumericSuffix;
7289 use kcl_api::Object;
7290 use kcl_api::Plane;
7291 use kcl_api::Sketch;
7292 use kcl_api::Tangent;
7293 use kcl_api::Vertical;
7294
7295 use super::*;
7296 use crate::engine::PlaneName;
7297 use crate::engine::engine_manager::EngineManager;
7298 use crate::execution::cache::SketchModeState;
7299 use crate::execution::cache::clear_mem_cache;
7300 use crate::execution::cache::read_old_memory;
7301 use crate::execution::cache::write_old_memory;
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;
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(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.restore_sketch_checkpoint(checkpoint_id).await.unwrap();
7740
7741 assert_eq!(restored.source_delta.text, expected_source);
7742 assert_eq!(restored.scene_graph_delta.new_graph, expected_scene_graph);
7743 assert!(restored.scene_graph_delta.invalidates_ids);
7744 assert_eq!(restored.scene_graph_delta.exec_outcome, expected_exec_outcome);
7745 assert_eq!(frontend.scene_graph, expected_scene_graph);
7746 assert_eq!(frontend.point_freedom_cache, expected_point_freedom_cache);
7747
7748 ctx.close().await;
7749 }
7750
7751 #[tokio::test(flavor = "multi_thread")]
7752 async fn test_sketch_checkpoints_prune_oldest_entries() {
7753 let mut frontend = FrontendState::new();
7754 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
7755 let mock_ctx = ExecutorContext::new_mock(None).await;
7756 let version = Version(0);
7757
7758 let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
7759 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7760
7761 let mut checkpoint_ids = Vec::new();
7762 for _ in 0..(MAX_SKETCH_CHECKPOINTS + 3) {
7763 checkpoint_ids.push(
7764 frontend
7765 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7766 .await
7767 .unwrap(),
7768 );
7769 }
7770
7771 assert_eq!(frontend.sketch_checkpoints.len(), MAX_SKETCH_CHECKPOINTS);
7772 assert!(checkpoint_ids.windows(2).all(|ids| ids[0] < ids[1]));
7773
7774 let oldest_retained = checkpoint_ids[3];
7775 assert_eq!(
7776 frontend.sketch_checkpoints.front().map(|checkpoint| checkpoint.id),
7777 Some(oldest_retained)
7778 );
7779
7780 let evicted_restore = frontend.restore_sketch_checkpoint(checkpoint_ids[0]).await;
7781 assert!(evicted_restore.is_err());
7782 assert!(evicted_restore.unwrap_err().msg.contains("Sketch checkpoint not found"));
7783
7784 frontend
7785 .restore_sketch_checkpoint(*checkpoint_ids.last().unwrap())
7786 .await
7787 .unwrap();
7788
7789 ctx.close().await;
7790 }
7791
7792 #[tokio::test(flavor = "multi_thread")]
7793 async fn test_restore_sketch_checkpoint_missing_id_returns_error() {
7794 let mut frontend = FrontendState::new();
7795 let missing_checkpoint = SketchCheckpointId::new(999);
7796
7797 let err = frontend
7798 .restore_sketch_checkpoint(missing_checkpoint)
7799 .await
7800 .expect_err("Expected restore to fail for missing checkpoint");
7801
7802 assert!(err.msg.contains("Sketch checkpoint not found"));
7803 }
7804
7805 #[tokio::test(flavor = "multi_thread")]
7806 async fn test_clear_sketch_checkpoints_removes_all_restore_points() {
7807 let mut frontend = FrontendState::new();
7808 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
7809 let mock_ctx = ExecutorContext::new_mock(None).await;
7810 let version = Version(0);
7811
7812 let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
7813 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7814
7815 let checkpoint_a = frontend
7816 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7817 .await
7818 .unwrap();
7819 let checkpoint_b = frontend
7820 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7821 .await
7822 .unwrap();
7823 assert_eq!(frontend.sketch_checkpoints.len(), 2);
7824
7825 frontend.clear_sketch_checkpoints();
7826 assert!(frontend.sketch_checkpoints.is_empty());
7827 frontend.restore_sketch_checkpoint(checkpoint_a).await.unwrap_err();
7828 frontend.restore_sketch_checkpoint(checkpoint_b).await.unwrap_err();
7829
7830 ctx.close().await;
7831 }
7832
7833 #[tokio::test(flavor = "multi_thread")]
7834 async fn test_hack_set_program_keeps_old_checkpoints_and_adds_fresh_baseline() {
7835 let mut frontend = FrontendState::new();
7836 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
7837 let mock_ctx = ExecutorContext::new_mock(None).await;
7838 let version = Version(0);
7839
7840 let (_sketch_id, _line_id, source_delta, scene_graph_delta) =
7841 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7842 let old_source = source_delta.text.clone();
7843 let old_checkpoint = frontend
7844 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7845 .await
7846 .unwrap();
7847 let initial_checkpoint_count = frontend.sketch_checkpoints.len();
7848
7849 let new_program = Program::parse("sketch(on = XY) {\n point(at = [1mm, 2mm])\n}\n")
7850 .unwrap()
7851 .0
7852 .unwrap();
7853
7854 let result = frontend.hack_set_program(&ctx, new_program).await.unwrap();
7855 let SetProgramOutcome::Success {
7856 checkpoint_id: Some(new_checkpoint),
7857 ..
7858 } = result
7859 else {
7860 panic!("Expected Success with a fresh checkpoint baseline");
7861 };
7862
7863 assert_eq!(frontend.sketch_checkpoints.len(), initial_checkpoint_count + 1);
7864
7865 let old_restore = frontend.restore_sketch_checkpoint(old_checkpoint).await.unwrap();
7866 assert_eq!(old_restore.source_delta.text, old_source);
7867
7868 let new_restore = frontend.restore_sketch_checkpoint(new_checkpoint).await.unwrap();
7869 assert!(new_restore.source_delta.text.contains("point(at = [1mm, 2mm])"));
7870
7871 ctx.close().await;
7872 }
7873
7874 #[tokio::test(flavor = "multi_thread")]
7875 async fn test_hack_set_program_exec_failure_does_not_add_checkpoint() {
7876 let mut frontend = FrontendState::new();
7877 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
7878 let mock_ctx = ExecutorContext::new_mock(None).await;
7879 let version = Version(0);
7880
7881 let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
7882 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7883 let old_checkpoint = frontend
7884 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7885 .await
7886 .unwrap();
7887 let checkpoint_count_before = frontend.sketch_checkpoints.len();
7888
7889 let failing_program = Program::parse(
7890 "sketch(on = XY) {\n line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])\n}\n\nbad = missing_name\n",
7891 )
7892 .unwrap()
7893 .0
7894 .unwrap();
7895
7896 let result = frontend.hack_set_program(&ctx, failing_program).await.unwrap();
7897 assert!(matches!(result, SetProgramOutcome::ExecFailure { .. }));
7898 assert_eq!(frontend.sketch_checkpoints.len(), checkpoint_count_before);
7899 frontend.restore_sketch_checkpoint(old_checkpoint).await.unwrap();
7900
7901 ctx.close().await;
7902 }
7903
7904 #[tokio::test(flavor = "multi_thread")]
7905 async fn test_restore_sketch_checkpoint_restores_and_clears_mock_memory() {
7906 let mut frontend = FrontendState::new();
7907 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
7908
7909 let program = Program::parse(
7910 "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",
7911 )
7912 .unwrap()
7913 .0
7914 .unwrap();
7915 let set_program_outcome = frontend.hack_set_program(&ctx, program).await.unwrap();
7916 let SetProgramOutcome::Success { exec_outcome, .. } = set_program_outcome else {
7917 panic!("Expected successful baseline program execution");
7918 };
7919
7920 clear_mem_cache().await;
7921 assert!(read_old_memory().await.is_none());
7922
7923 let checkpoint_without_mock_memory = frontend
7924 .create_sketch_checkpoint((*exec_outcome).clone())
7925 .await
7926 .unwrap();
7927
7928 write_old_memory(SketchModeState::new_for_tests()).await;
7929 assert!(read_old_memory().await.is_some());
7930
7931 let checkpoint_with_mock_memory = frontend
7932 .create_sketch_checkpoint((*exec_outcome).clone())
7933 .await
7934 .unwrap();
7935
7936 clear_mem_cache().await;
7937 assert!(read_old_memory().await.is_none());
7938
7939 frontend
7940 .restore_sketch_checkpoint(checkpoint_with_mock_memory)
7941 .await
7942 .unwrap();
7943 assert!(read_old_memory().await.is_some());
7944
7945 frontend
7946 .restore_sketch_checkpoint(checkpoint_without_mock_memory)
7947 .await
7948 .unwrap();
7949 assert!(read_old_memory().await.is_none());
7950
7951 ctx.close().await;
7952 }
7953
7954 #[tokio::test(flavor = "multi_thread")]
7955 async fn test_hack_set_program_exec_error_still_allows_edit_sketch() {
7956 let source = "\
7957sketch(on = XY) {
7958 line1 = line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])
7959}
7960
7961bad = missing_name
7962";
7963 let program = Program::parse(source).unwrap().0.unwrap();
7964
7965 let mut frontend = FrontendState::new();
7966
7967 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
7968 let mock_ctx = ExecutorContext::new_mock(None).await;
7969 let version = Version(0);
7970 let project_id = ProjectId(0);
7971 let file_id = FileId(0);
7972
7973 let SetProgramOutcome::ExecFailure { .. } = frontend.hack_set_program(&ctx, program).await.unwrap() else {
7974 panic!("Expected ExecFailure from hack_set_program due to syntax error in program");
7975 };
7976
7977 let sketch_id = frontend
7978 .scene_graph
7979 .objects
7980 .iter()
7981 .find_map(|obj| matches!(obj.kind, ObjectKind::Sketch(_)).then_some(obj.id))
7982 .expect("Expected sketch object from errored hack_set_program");
7983
7984 frontend
7985 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
7986 .await
7987 .unwrap();
7988
7989 ctx.close().await;
7990 mock_ctx.close().await;
7991 }
7992
7993 #[tokio::test(flavor = "multi_thread")]
7994 async fn test_new_sketch_add_point_edit_point() {
7995 let program = Program::empty();
7996
7997 let mut frontend = FrontendState::new();
7998 frontend.program = program;
7999
8000 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
8001 let mock_ctx = ExecutorContext::new_mock(None).await;
8002 let version = Version(0);
8003
8004 let sketch_args = SketchCtor {
8005 on: Plane::Default(PlaneName::Xy),
8006 };
8007 let (_src_delta, scene_delta, sketch_id) = frontend
8008 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
8009 .await
8010 .unwrap();
8011 assert_eq!(sketch_id, ObjectId(1));
8012 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
8013 let sketch_object = &scene_delta.new_graph.objects[1];
8014 assert_eq!(sketch_object.id, ObjectId(1));
8015 assert_eq!(
8016 sketch_object.kind,
8017 ObjectKind::Sketch(Sketch {
8018 args: SketchCtor {
8019 on: Plane::Default(PlaneName::Xy)
8020 },
8021 plane: ObjectId(0),
8022 segments: vec![],
8023 constraints: vec![],
8024 })
8025 );
8026 assert_eq!(scene_delta.new_graph.objects.len(), 2);
8027
8028 let point_ctor = PointCtor {
8029 position: Point2d {
8030 x: Expr::Number(Number {
8031 value: 1.0,
8032 units: NumericSuffix::Inch,
8033 }),
8034 y: Expr::Number(Number {
8035 value: 2.0,
8036 units: NumericSuffix::Inch,
8037 }),
8038 },
8039 };
8040 let segment = SegmentCtor::Point(point_ctor);
8041 let (src_delta, scene_delta) = frontend
8042 .add_segment(&mock_ctx, version, sketch_id, segment, None)
8043 .await
8044 .unwrap();
8045 insta::assert_snapshot!("test_new_sketch_add_point_edit_point_1", src_delta.text.as_str());
8046 assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
8047 assert_eq!(scene_delta.new_graph.objects.len(), 3);
8048 for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
8049 assert_eq!(scene_object.id.0, i);
8050 }
8051
8052 let point_id = *scene_delta.new_objects.last().unwrap();
8053
8054 let point_ctor = PointCtor {
8055 position: Point2d {
8056 x: Expr::Number(Number {
8057 value: 3.0,
8058 units: NumericSuffix::Inch,
8059 }),
8060 y: Expr::Number(Number {
8061 value: 4.0,
8062 units: NumericSuffix::Inch,
8063 }),
8064 },
8065 };
8066 let segments = vec![ExistingSegmentCtor {
8067 id: point_id,
8068 ctor: SegmentCtor::Point(point_ctor),
8069 }];
8070 let (src_delta, scene_delta) = frontend
8071 .edit_segments(&mock_ctx, version, sketch_id, segments)
8072 .await
8073 .unwrap();
8074 insta::assert_snapshot!("test_new_sketch_add_point_edit_point_2", src_delta.text.as_str());
8075 assert_eq!(scene_delta.new_objects, vec![]);
8076 assert_eq!(scene_delta.new_graph.objects.len(), 3);
8077
8078 ctx.close().await;
8079 mock_ctx.close().await;
8080 }
8081
8082 #[tokio::test(flavor = "multi_thread")]
8083 async fn test_new_sketch_add_line_edit_line() {
8084 let program = Program::empty();
8085
8086 let mut frontend = FrontendState::new();
8087 frontend.program = program;
8088
8089 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
8090 let mock_ctx = ExecutorContext::new_mock(None).await;
8091 let version = Version(0);
8092
8093 let sketch_args = SketchCtor {
8094 on: Plane::Default(PlaneName::Xy),
8095 };
8096 let (_src_delta, scene_delta, sketch_id) = frontend
8097 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
8098 .await
8099 .unwrap();
8100 assert_eq!(sketch_id, ObjectId(1));
8101 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
8102 let sketch_object = &scene_delta.new_graph.objects[1];
8103 assert_eq!(sketch_object.id, ObjectId(1));
8104 assert_eq!(
8105 sketch_object.kind,
8106 ObjectKind::Sketch(Sketch {
8107 args: SketchCtor {
8108 on: Plane::Default(PlaneName::Xy)
8109 },
8110 plane: ObjectId(0),
8111 segments: vec![],
8112 constraints: vec![],
8113 })
8114 );
8115 assert_eq!(scene_delta.new_graph.objects.len(), 2);
8116
8117 let line_ctor = LineCtor {
8118 start: Point2d {
8119 x: Expr::Number(Number {
8120 value: 0.0,
8121 units: NumericSuffix::Mm,
8122 }),
8123 y: Expr::Number(Number {
8124 value: 0.0,
8125 units: NumericSuffix::Mm,
8126 }),
8127 },
8128 end: Point2d {
8129 x: Expr::Number(Number {
8130 value: 10.0,
8131 units: NumericSuffix::Mm,
8132 }),
8133 y: Expr::Number(Number {
8134 value: 10.0,
8135 units: NumericSuffix::Mm,
8136 }),
8137 },
8138 construction: None,
8139 };
8140 let segment = SegmentCtor::Line(line_ctor);
8141 let (src_delta, scene_delta) = frontend
8142 .add_segment(&mock_ctx, version, sketch_id, segment, None)
8143 .await
8144 .unwrap();
8145 insta::assert_snapshot!("test_new_sketch_add_line_edit_line_1", src_delta.text.as_str());
8146 assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
8147 assert_eq!(scene_delta.new_graph.objects.len(), 5);
8148 for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
8149 assert_eq!(scene_object.id.0, i);
8150 }
8151
8152 let line = *scene_delta.new_objects.last().unwrap();
8154
8155 let line_ctor = LineCtor {
8156 start: Point2d {
8157 x: Expr::Number(Number {
8158 value: 1.0,
8159 units: NumericSuffix::Mm,
8160 }),
8161 y: Expr::Number(Number {
8162 value: 2.0,
8163 units: NumericSuffix::Mm,
8164 }),
8165 },
8166 end: Point2d {
8167 x: Expr::Number(Number {
8168 value: 13.0,
8169 units: NumericSuffix::Mm,
8170 }),
8171 y: Expr::Number(Number {
8172 value: 14.0,
8173 units: NumericSuffix::Mm,
8174 }),
8175 },
8176 construction: None,
8177 };
8178 let segments = vec![ExistingSegmentCtor {
8179 id: line,
8180 ctor: SegmentCtor::Line(line_ctor),
8181 }];
8182 let (src_delta, scene_delta) = frontend
8183 .edit_segments(&mock_ctx, version, sketch_id, segments)
8184 .await
8185 .unwrap();
8186 insta::assert_snapshot!("test_new_sketch_add_line_edit_line_2", src_delta.text.as_str());
8187 assert_eq!(scene_delta.new_objects, vec![]);
8188 assert_eq!(scene_delta.new_graph.objects.len(), 5);
8189
8190 ctx.close().await;
8191 mock_ctx.close().await;
8192 }
8193
8194 #[tokio::test(flavor = "multi_thread")]
8195 async fn test_new_sketch_add_arc_edit_arc() {
8196 let program = Program::empty();
8197
8198 let mut frontend = FrontendState::new();
8199 frontend.program = program;
8200
8201 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
8202 let mock_ctx = ExecutorContext::new_mock(None).await;
8203 let version = Version(0);
8204
8205 let sketch_args = SketchCtor {
8206 on: Plane::Default(PlaneName::Xy),
8207 };
8208 let (_src_delta, scene_delta, sketch_id) = frontend
8209 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
8210 .await
8211 .unwrap();
8212 assert_eq!(sketch_id, ObjectId(1));
8213 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
8214 let sketch_object = &scene_delta.new_graph.objects[1];
8215 assert_eq!(sketch_object.id, ObjectId(1));
8216 assert_eq!(
8217 sketch_object.kind,
8218 ObjectKind::Sketch(Sketch {
8219 args: SketchCtor {
8220 on: Plane::Default(PlaneName::Xy),
8221 },
8222 plane: ObjectId(0),
8223 segments: vec![],
8224 constraints: vec![],
8225 })
8226 );
8227 assert_eq!(scene_delta.new_graph.objects.len(), 2);
8228
8229 let arc_ctor = ArcCtor {
8230 start: Point2d {
8231 x: Expr::Var(Number {
8232 value: 0.0,
8233 units: NumericSuffix::Mm,
8234 }),
8235 y: Expr::Var(Number {
8236 value: 0.0,
8237 units: NumericSuffix::Mm,
8238 }),
8239 },
8240 end: Point2d {
8241 x: Expr::Var(Number {
8242 value: 10.0,
8243 units: NumericSuffix::Mm,
8244 }),
8245 y: Expr::Var(Number {
8246 value: 10.0,
8247 units: NumericSuffix::Mm,
8248 }),
8249 },
8250 center: Point2d {
8251 x: Expr::Var(Number {
8252 value: 10.0,
8253 units: NumericSuffix::Mm,
8254 }),
8255 y: Expr::Var(Number {
8256 value: 0.0,
8257 units: NumericSuffix::Mm,
8258 }),
8259 },
8260 direction: None,
8261 construction: None,
8262 };
8263 let segment = SegmentCtor::Arc(arc_ctor);
8264 let (src_delta, scene_delta) = frontend
8265 .add_segment(&mock_ctx, version, sketch_id, segment, None)
8266 .await
8267 .unwrap();
8268 insta::assert_snapshot!("test_new_sketch_add_arc_edit_arc_1", src_delta.text.as_str());
8269 assert_eq!(
8270 scene_delta.new_objects,
8271 vec![ObjectId(2), ObjectId(3), ObjectId(4), ObjectId(5)]
8272 );
8273 for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
8274 assert_eq!(scene_object.id.0, i);
8275 }
8276 assert_eq!(scene_delta.new_graph.objects.len(), 6);
8277
8278 let arc = *scene_delta.new_objects.last().unwrap();
8280
8281 let arc_ctor = ArcCtor {
8282 start: Point2d {
8283 x: Expr::Var(Number {
8284 value: 1.0,
8285 units: NumericSuffix::Mm,
8286 }),
8287 y: Expr::Var(Number {
8288 value: 2.0,
8289 units: NumericSuffix::Mm,
8290 }),
8291 },
8292 end: Point2d {
8293 x: Expr::Var(Number {
8294 value: 13.0,
8295 units: NumericSuffix::Mm,
8296 }),
8297 y: Expr::Var(Number {
8298 value: 14.0,
8299 units: NumericSuffix::Mm,
8300 }),
8301 },
8302 center: Point2d {
8303 x: Expr::Var(Number {
8304 value: 13.0,
8305 units: NumericSuffix::Mm,
8306 }),
8307 y: Expr::Var(Number {
8308 value: 2.0,
8309 units: NumericSuffix::Mm,
8310 }),
8311 },
8312 direction: None,
8313 construction: None,
8314 };
8315 let segments = vec![ExistingSegmentCtor {
8316 id: arc,
8317 ctor: SegmentCtor::Arc(arc_ctor),
8318 }];
8319 let (src_delta, scene_delta) = frontend
8320 .edit_segments(&mock_ctx, version, sketch_id, segments)
8321 .await
8322 .unwrap();
8323 insta::assert_snapshot!("test_new_sketch_add_arc_edit_arc_2", src_delta.text.as_str());
8324 assert_eq!(scene_delta.new_objects, vec![]);
8325 assert_eq!(scene_delta.new_graph.objects.len(), 6);
8326
8327 ctx.close().await;
8328 mock_ctx.close().await;
8329 }
8330
8331 #[tokio::test(flavor = "multi_thread")]
8332 async fn test_new_sketch_add_circle_edit_circle() {
8333 let program = Program::empty();
8334
8335 let mut frontend = FrontendState::new();
8336 frontend.program = program;
8337
8338 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
8339 let mock_ctx = ExecutorContext::new_mock(None).await;
8340 let version = Version(0);
8341
8342 let sketch_args = SketchCtor {
8343 on: Plane::Default(PlaneName::Xy),
8344 };
8345 let (_src_delta, _scene_delta, sketch_id) = frontend
8346 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
8347 .await
8348 .unwrap();
8349
8350 let circle_ctor = CircleCtor {
8352 start: Point2d {
8353 x: Expr::Var(Number {
8354 value: 5.0,
8355 units: NumericSuffix::Mm,
8356 }),
8357 y: Expr::Var(Number {
8358 value: 0.0,
8359 units: NumericSuffix::Mm,
8360 }),
8361 },
8362 center: Point2d {
8363 x: Expr::Var(Number {
8364 value: 0.0,
8365 units: NumericSuffix::Mm,
8366 }),
8367 y: Expr::Var(Number {
8368 value: 0.0,
8369 units: NumericSuffix::Mm,
8370 }),
8371 },
8372 construction: None,
8373 };
8374 let segment = SegmentCtor::Circle(circle_ctor);
8375 let (src_delta, scene_delta) = frontend
8376 .add_segment(&mock_ctx, version, sketch_id, segment, None)
8377 .await
8378 .unwrap();
8379 insta::assert_snapshot!("test_new_sketch_add_circle_edit_circle_1", src_delta.text.as_str());
8380 assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
8382 assert_eq!(scene_delta.new_graph.objects.len(), 5);
8383
8384 let circle = *scene_delta.new_objects.last().unwrap();
8385
8386 let circle_ctor = CircleCtor {
8388 start: Point2d {
8389 x: Expr::Var(Number {
8390 value: 10.0,
8391 units: NumericSuffix::Mm,
8392 }),
8393 y: Expr::Var(Number {
8394 value: 0.0,
8395 units: NumericSuffix::Mm,
8396 }),
8397 },
8398 center: Point2d {
8399 x: Expr::Var(Number {
8400 value: 3.0,
8401 units: NumericSuffix::Mm,
8402 }),
8403 y: Expr::Var(Number {
8404 value: 4.0,
8405 units: NumericSuffix::Mm,
8406 }),
8407 },
8408 construction: None,
8409 };
8410 let segments = vec![ExistingSegmentCtor {
8411 id: circle,
8412 ctor: SegmentCtor::Circle(circle_ctor),
8413 }];
8414 let (src_delta, scene_delta) = frontend
8415 .edit_segments(&mock_ctx, version, sketch_id, segments)
8416 .await
8417 .unwrap();
8418 insta::assert_snapshot!("test_new_sketch_add_circle_edit_circle_2", src_delta.text.as_str());
8419 assert_eq!(scene_delta.new_objects, vec![]);
8420 assert_eq!(scene_delta.new_graph.objects.len(), 5);
8421
8422 ctx.close().await;
8423 mock_ctx.close().await;
8424 }
8425
8426 #[tokio::test(flavor = "multi_thread")]
8427 async fn test_delete_circle() {
8428 let initial_source = "sketch001 = sketch(on = XY) {
8429 circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
8430}
8431";
8432
8433 let program = Program::parse(initial_source).unwrap().0.unwrap();
8434 let mut frontend = FrontendState::new();
8435
8436 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
8437 let mock_ctx = ExecutorContext::new_mock(None).await;
8438 let version = Version(0);
8439
8440 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8441 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8442 let sketch_id = sketch_object.id;
8443 let sketch = expect_sketch(sketch_object);
8444
8445 assert_eq!(sketch.segments.len(), 3);
8447 let circle_id = sketch.segments[2];
8448
8449 let (src_delta, scene_delta) = frontend
8451 .delete_objects(&mock_ctx, version, sketch_id, vec![], vec![circle_id])
8452 .await
8453 .unwrap();
8454 insta::assert_snapshot!("test_delete_circle", src_delta.text.as_str());
8455 let new_sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8456 let new_sketch = expect_sketch(new_sketch_object);
8457 assert_eq!(new_sketch.segments.len(), 0);
8458
8459 ctx.close().await;
8460 mock_ctx.close().await;
8461 }
8462
8463 #[tokio::test(flavor = "multi_thread")]
8464 async fn test_edit_circle_via_point() {
8465 let initial_source = "sketch001 = sketch(on = XY) {
8466 circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
8467}
8468";
8469
8470 let program = Program::parse(initial_source).unwrap().0.unwrap();
8471 let mut frontend = FrontendState::new();
8472
8473 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
8474 let mock_ctx = ExecutorContext::new_mock(None).await;
8475 let version = Version(0);
8476
8477 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8478 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8479 let sketch_id = sketch_object.id;
8480 let sketch = expect_sketch(sketch_object);
8481
8482 let circle_id = sketch
8484 .segments
8485 .iter()
8486 .copied()
8487 .find(|seg_id| {
8488 matches!(
8489 &frontend.scene_graph.objects[seg_id.0].kind,
8490 ObjectKind::Segment {
8491 segment: Segment::Circle(_)
8492 }
8493 )
8494 })
8495 .expect("Expected a circle segment in sketch");
8496 let circle_object = &frontend.scene_graph.objects[circle_id.0];
8497 let ObjectKind::Segment {
8498 segment: Segment::Circle(circle),
8499 } = &circle_object.kind
8500 else {
8501 panic!("Expected circle segment, got: {:?}", circle_object.kind);
8502 };
8503 let start_point_id = circle.start;
8504
8505 let segments = vec![ExistingSegmentCtor {
8507 id: start_point_id,
8508 ctor: SegmentCtor::Point(PointCtor {
8509 position: Point2d {
8510 x: Expr::Var(Number {
8511 value: 7.0,
8512 units: NumericSuffix::Mm,
8513 }),
8514 y: Expr::Var(Number {
8515 value: 1.0,
8516 units: NumericSuffix::Mm,
8517 }),
8518 },
8519 }),
8520 }];
8521 let (src_delta, _scene_delta) = frontend
8522 .edit_segments(&mock_ctx, version, sketch_id, segments)
8523 .await
8524 .unwrap();
8525 insta::assert_snapshot!("test_edit_circle_via_point", src_delta.text.as_str());
8526
8527 ctx.close().await;
8528 mock_ctx.close().await;
8529 }
8530
8531 #[tokio::test(flavor = "multi_thread")]
8532 async fn test_add_line_when_sketch_block_uses_variable() {
8533 let initial_source = "s = sketch(on = XY) {}
8534";
8535
8536 let program = Program::parse(initial_source).unwrap().0.unwrap();
8537
8538 let mut frontend = FrontendState::new();
8539
8540 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
8541 let mock_ctx = ExecutorContext::new_mock(None).await;
8542 let version = Version(0);
8543
8544 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8545 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8546 let sketch_id = sketch_object.id;
8547
8548 let line_ctor = LineCtor {
8549 start: Point2d {
8550 x: Expr::Number(Number {
8551 value: 0.0,
8552 units: NumericSuffix::Mm,
8553 }),
8554 y: Expr::Number(Number {
8555 value: 0.0,
8556 units: NumericSuffix::Mm,
8557 }),
8558 },
8559 end: Point2d {
8560 x: Expr::Number(Number {
8561 value: 10.0,
8562 units: NumericSuffix::Mm,
8563 }),
8564 y: Expr::Number(Number {
8565 value: 10.0,
8566 units: NumericSuffix::Mm,
8567 }),
8568 },
8569 construction: None,
8570 };
8571 let segment = SegmentCtor::Line(line_ctor);
8572 let (src_delta, scene_delta) = frontend
8573 .add_segment(&mock_ctx, version, sketch_id, segment, None)
8574 .await
8575 .unwrap();
8576 insta::assert_snapshot!("test_add_line_when_sketch_block_uses_variable", src_delta.text.as_str());
8577 assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
8578 assert_eq!(scene_delta.new_graph.objects.len(), 5);
8579
8580 ctx.close().await;
8581 mock_ctx.close().await;
8582 }
8583
8584 #[tokio::test(flavor = "multi_thread")]
8585 async fn test_new_sketch_add_line_delete_sketch() {
8586 let program = Program::empty();
8587
8588 let mut frontend = FrontendState::new();
8589 frontend.program = program;
8590
8591 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
8592 let mock_ctx = ExecutorContext::new_mock(None).await;
8593 let version = Version(0);
8594
8595 let sketch_args = SketchCtor {
8596 on: Plane::Default(PlaneName::Xy),
8597 };
8598 let (_src_delta, scene_delta, sketch_id) = frontend
8599 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
8600 .await
8601 .unwrap();
8602 assert_eq!(sketch_id, ObjectId(1));
8603 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
8604 let sketch_object = &scene_delta.new_graph.objects[1];
8605 assert_eq!(sketch_object.id, ObjectId(1));
8606 assert_eq!(
8607 sketch_object.kind,
8608 ObjectKind::Sketch(Sketch {
8609 args: SketchCtor {
8610 on: Plane::Default(PlaneName::Xy)
8611 },
8612 plane: ObjectId(0),
8613 segments: vec![],
8614 constraints: vec![],
8615 })
8616 );
8617 assert_eq!(scene_delta.new_graph.objects.len(), 2);
8618
8619 let line_ctor = LineCtor {
8620 start: Point2d {
8621 x: Expr::Number(Number {
8622 value: 0.0,
8623 units: NumericSuffix::Mm,
8624 }),
8625 y: Expr::Number(Number {
8626 value: 0.0,
8627 units: NumericSuffix::Mm,
8628 }),
8629 },
8630 end: Point2d {
8631 x: Expr::Number(Number {
8632 value: 10.0,
8633 units: NumericSuffix::Mm,
8634 }),
8635 y: Expr::Number(Number {
8636 value: 10.0,
8637 units: NumericSuffix::Mm,
8638 }),
8639 },
8640 construction: None,
8641 };
8642 let segment = SegmentCtor::Line(line_ctor);
8643 let (src_delta, scene_delta) = frontend
8644 .add_segment(&mock_ctx, version, sketch_id, segment, None)
8645 .await
8646 .unwrap();
8647 insta::assert_snapshot!("test_new_sketch_add_line_delete_sketch_1", src_delta.text.as_str());
8648 assert_eq!(scene_delta.new_graph.objects.len(), 5);
8649
8650 let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8651 insta::assert_snapshot!("test_new_sketch_add_line_delete_sketch_2", src_delta.text.as_str());
8652 assert_eq!(scene_delta.new_graph.objects.len(), 0);
8653
8654 ctx.close().await;
8655 mock_ctx.close().await;
8656 }
8657
8658 #[tokio::test(flavor = "multi_thread")]
8659 async fn test_delete_sketch_when_sketch_block_uses_variable() {
8660 let initial_source = "s = sketch(on = XY) {}
8661";
8662
8663 let program = Program::parse(initial_source).unwrap().0.unwrap();
8664
8665 let mut frontend = FrontendState::new();
8666
8667 let ctx = ExecutorContext::new_with_engine(sync::Arc::new(EngineManager::new_mock()), Default::default());
8668 let version = Version(0);
8669
8670 frontend.hack_set_program(&ctx, program).await.unwrap();
8671 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8672 let sketch_id = sketch_object.id;
8673
8674 let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8675 insta::assert_snapshot!(
8676 "test_delete_sketch_when_sketch_block_uses_variable",
8677 src_delta.text.as_str()
8678 );
8679 assert_eq!(scene_delta.new_graph.objects.len(), 0);
8680
8681 ctx.close().await;
8682 }
8683
8684 #[tokio::test(flavor = "multi_thread")]
8685 async fn test_delete_sketch_after_comment() {
8686 let initial_source = "sketch001 = sketch(on = XZ) {
8687}
8688";
8689
8690 let program = Program::parse(initial_source).unwrap().0.unwrap();
8691 let mut frontend = FrontendState::new();
8692
8693 let ctx = ExecutorContext::new_with_engine(sync::Arc::new(EngineManager::new_mock()), Default::default());
8694 let version = Version(0);
8695
8696 frontend.hack_set_program(&ctx, program).await.unwrap();
8697 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8698 let sketch_id = sketch_object.id;
8699 let original_source = sketch_object.source.clone();
8700
8701 let commented_source = "// test 1
8702sketch001 = sketch(on = XZ) {
8703}
8704";
8705 let commented_program = Program::parse(commented_source).unwrap().0.unwrap();
8706 frontend.engine_execute(&ctx, commented_program).await.unwrap();
8707
8708 let cached_sketch_object = &frontend.scene_graph.objects[sketch_id.0];
8709 assert_eq!(cached_sketch_object.source, original_source);
8710
8711 let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8712 assert!(
8713 !src_delta.text.contains("sketch001"),
8714 "sketch was not deleted: {}",
8715 src_delta.text
8716 );
8717 insta::assert_snapshot!("test_delete_sketch_after_comment", src_delta.text.as_str());
8719 assert_eq!(scene_delta.new_graph.objects.len(), 0);
8720
8721 ctx.close().await;
8722 }
8723
8724 #[tokio::test(flavor = "multi_thread")]
8725 async fn test_delete_sketch_preserves_pre_comment_when_followed_by_code() {
8726 let initial_source = "sketch001 = sketch(on = XZ) {
8727}
8728foo = 1
8729";
8730
8731 let program = Program::parse(initial_source).unwrap().0.unwrap();
8732 let mut frontend = FrontendState::new();
8733
8734 let ctx = ExecutorContext::new_with_engine(sync::Arc::new(EngineManager::new_mock()), Default::default());
8735 let version = Version(0);
8736
8737 frontend.hack_set_program(&ctx, program).await.unwrap();
8738 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8739 let sketch_id = sketch_object.id;
8740
8741 let commented_source = "// keep me
8742sketch001 = sketch(on = XZ) {
8743}
8744foo = 1
8745";
8746 let commented_program = Program::parse(commented_source).unwrap().0.unwrap();
8747 frontend.engine_execute(&ctx, commented_program).await.unwrap();
8748
8749 let (src_delta, _scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8750 insta::assert_snapshot!(
8752 "test_delete_sketch_preserves_pre_comment_when_followed_by_code",
8753 src_delta.text.as_str()
8754 );
8755
8756 ctx.close().await;
8757 }
8758
8759 #[tokio::test(flavor = "multi_thread")]
8760 async fn test_delete_segment_preserves_pre_comment() {
8761 let initial_source = "\
8762sketch(on = XY) {
8763 point(at = [var 1, var 2])
8764 // describe the middle point
8765 point(at = [var 3, var 4])
8766 point(at = [var 5, var 6])
8767}
8768";
8769
8770 let program = Program::parse(initial_source).unwrap().0.unwrap();
8771 let mut frontend = FrontendState::new();
8772
8773 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
8774 let mock_ctx = ExecutorContext::new_mock(None).await;
8775 let version = Version(0);
8776
8777 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8778 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8779 let sketch_id = sketch_object.id;
8780 let sketch = expect_sketch(sketch_object);
8781
8782 let middle_point_id = *sketch.segments.get(1).unwrap();
8783
8784 let (src_delta, _scene_delta) = frontend
8785 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![middle_point_id])
8786 .await
8787 .unwrap();
8788 insta::assert_snapshot!("test_delete_segment_preserves_pre_comment", src_delta.text.as_str());
8791
8792 ctx.close().await;
8793 mock_ctx.close().await;
8794 }
8795
8796 #[tokio::test(flavor = "multi_thread")]
8797 async fn test_delete_last_segment_preserves_pre_comment() {
8798 let initial_source = "\
8799sketch(on = XY) {
8800 point(at = [var 1, var 2])
8801 // describe the trailing point
8802 point(at = [var 3, var 4])
8803}
8804";
8805
8806 let program = Program::parse(initial_source).unwrap().0.unwrap();
8807 let mut frontend = FrontendState::new();
8808
8809 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
8810 let mock_ctx = ExecutorContext::new_mock(None).await;
8811 let version = Version(0);
8812
8813 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8814 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8815 let sketch_id = sketch_object.id;
8816 let sketch = expect_sketch(sketch_object);
8817
8818 let last_point_id = *sketch.segments.last().unwrap();
8819
8820 let (src_delta, _scene_delta) = frontend
8821 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![last_point_id])
8822 .await
8823 .unwrap();
8824 insta::assert_snapshot!(
8827 "test_delete_last_segment_preserves_pre_comment",
8828 src_delta.text.as_str()
8829 );
8830
8831 ctx.close().await;
8832 mock_ctx.close().await;
8833 }
8834
8835 #[tokio::test(flavor = "multi_thread")]
8836 async fn test_delete_segment_drops_inline_trailing_comment() {
8837 let initial_source = "\
8838sketch(on = XY) {
8839 point(at = [var 1, var 2])
8840 point(at = [var 3, var 4]) // same-line note that gets dropped
8841 point(at = [var 5, var 6])
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;
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 middle_point_id = *sketch.segments.get(1).unwrap();
8858
8859 let (src_delta, _scene_delta) = frontend
8860 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![middle_point_id])
8861 .await
8862 .unwrap();
8863 assert!(
8865 !src_delta.text.contains("same-line note"),
8866 "inline comment should have been removed: {}",
8867 src_delta.text
8868 );
8869
8870 ctx.close().await;
8871 mock_ctx.close().await;
8872 }
8873
8874 #[tokio::test(flavor = "multi_thread")]
8875 async fn test_delete_segments_preserves_block_comments_across_positions() {
8876 let initial_source = "\
8884sketch(on = XY) {
8885 /* above first - moves to middle */
8886 point(at = [var 1, var 2]) /* same-line on first - dropped */
8887 /* above middle - stays */
8888 point(at = [var 3, var 4])
8889 /* above last - moves to trailing meta */
8890 point(at = [var 5, var 6])
8891}
8892";
8893
8894 let program = Program::parse(initial_source).unwrap().0.unwrap();
8895 let mut frontend = FrontendState::new();
8896
8897 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
8898 let mock_ctx = ExecutorContext::new_mock(None).await;
8899 let version = Version(0);
8900
8901 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8902 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8903 let sketch_id = sketch_object.id;
8904 let sketch = expect_sketch(sketch_object);
8905
8906 let first_point_id = *sketch.segments.first().unwrap();
8907 let last_point_id = *sketch.segments.last().unwrap();
8908
8909 let (src_delta, _scene_delta) = frontend
8910 .delete_objects(
8911 &mock_ctx,
8912 version,
8913 sketch_id,
8914 Vec::new(),
8915 vec![first_point_id, last_point_id],
8916 )
8917 .await
8918 .unwrap();
8919 insta::assert_snapshot!(
8920 "test_delete_segments_preserves_block_comments_across_positions",
8921 src_delta.text.as_str()
8922 );
8923
8924 ctx.close().await;
8925 mock_ctx.close().await;
8926 }
8927
8928 #[tokio::test(flavor = "multi_thread")]
8929 async fn test_edit_line_when_editing_its_start_point() {
8930 let initial_source = "\
8931sketch(on = XY) {
8932 line(start = [var 1, var 2], end = [var 3, var 4])
8933}
8934";
8935
8936 let program = Program::parse(initial_source).unwrap().0.unwrap();
8937
8938 let mut frontend = FrontendState::new();
8939
8940 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
8941 let mock_ctx = ExecutorContext::new_mock(None).await;
8942 let version = Version(0);
8943
8944 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8945 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8946 let sketch_id = sketch_object.id;
8947 let sketch = expect_sketch(sketch_object);
8948
8949 let point_id = *sketch.segments.first().unwrap();
8950
8951 let point_ctor = PointCtor {
8952 position: Point2d {
8953 x: Expr::Var(Number {
8954 value: 5.0,
8955 units: NumericSuffix::Inch,
8956 }),
8957 y: Expr::Var(Number {
8958 value: 6.0,
8959 units: NumericSuffix::Inch,
8960 }),
8961 },
8962 };
8963 let segments = vec![ExistingSegmentCtor {
8964 id: point_id,
8965 ctor: SegmentCtor::Point(point_ctor),
8966 }];
8967 let (src_delta, scene_delta) = frontend
8968 .edit_segments(&mock_ctx, version, sketch_id, segments)
8969 .await
8970 .unwrap();
8971 insta::assert_snapshot!("test_edit_line_when_editing_its_start_point", src_delta.text.as_str());
8972 assert_eq!(scene_delta.new_objects, vec![]);
8973 assert_eq!(scene_delta.new_graph.objects.len(), 5);
8974
8975 ctx.close().await;
8976 mock_ctx.close().await;
8977 }
8978
8979 #[tokio::test(flavor = "multi_thread")]
8980 async fn test_edit_line_when_editing_its_end_point() {
8981 let initial_source = "\
8982sketch(on = XY) {
8983 line(start = [var 1, var 2], end = [var 3, var 4])
8984}
8985";
8986
8987 let program = Program::parse(initial_source).unwrap().0.unwrap();
8988
8989 let mut frontend = FrontendState::new();
8990
8991 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
8992 let mock_ctx = ExecutorContext::new_mock(None).await;
8993 let version = Version(0);
8994
8995 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8996 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8997 let sketch_id = sketch_object.id;
8998 let sketch = expect_sketch(sketch_object);
8999 let point_id = *sketch.segments.get(1).unwrap();
9000
9001 let point_ctor = PointCtor {
9002 position: Point2d {
9003 x: Expr::Var(Number {
9004 value: 5.0,
9005 units: NumericSuffix::Inch,
9006 }),
9007 y: Expr::Var(Number {
9008 value: 6.0,
9009 units: NumericSuffix::Inch,
9010 }),
9011 },
9012 };
9013 let segments = vec![ExistingSegmentCtor {
9014 id: point_id,
9015 ctor: SegmentCtor::Point(point_ctor),
9016 }];
9017 let (src_delta, scene_delta) = frontend
9018 .edit_segments(&mock_ctx, version, sketch_id, segments)
9019 .await
9020 .unwrap();
9021 insta::assert_snapshot!("test_edit_line_when_editing_its_end_point", src_delta.text.as_str());
9022 assert_eq!(scene_delta.new_objects, vec![]);
9023 assert_eq!(
9024 scene_delta.new_graph.objects.len(),
9025 5,
9026 "{:#?}",
9027 scene_delta.new_graph.objects
9028 );
9029
9030 ctx.close().await;
9031 mock_ctx.close().await;
9032 }
9033
9034 #[tokio::test(flavor = "multi_thread")]
9035 async fn test_edit_line_with_coincident_feedback() {
9036 let initial_source = "\
9037sketch(on = XY) {
9038 line1 = line(start = [var 1, var 2], end = [var 1, var 2])
9039 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9040 fixed([line1.start, [0, 0]])
9041 coincident([line1.end, line2.start])
9042 equalLength([line1, line2])
9043}
9044";
9045
9046 let program = Program::parse(initial_source).unwrap().0.unwrap();
9047
9048 let mut frontend = FrontendState::new();
9049
9050 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
9051 let mock_ctx = ExecutorContext::new_mock(None).await;
9052 let version = Version(0);
9053
9054 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9055 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9056 let sketch_id = sketch_object.id;
9057 let sketch = expect_sketch(sketch_object);
9058 let line2_end_id = *sketch.segments.get(4).unwrap();
9059
9060 let segments = vec![ExistingSegmentCtor {
9061 id: line2_end_id,
9062 ctor: SegmentCtor::Point(PointCtor {
9063 position: Point2d {
9064 x: Expr::Var(Number {
9065 value: 9.0,
9066 units: NumericSuffix::None,
9067 }),
9068 y: Expr::Var(Number {
9069 value: 10.0,
9070 units: NumericSuffix::None,
9071 }),
9072 },
9073 }),
9074 }];
9075 let (src_delta, scene_delta) = frontend
9076 .edit_segments(&mock_ctx, version, sketch_id, segments)
9077 .await
9078 .unwrap();
9079 insta::assert_snapshot!("test_edit_line_with_coincident_feedback", src_delta.text.as_str());
9080 assert_eq!(
9081 scene_delta.new_graph.objects.len(),
9082 11,
9083 "{:#?}",
9084 scene_delta.new_graph.objects
9085 );
9086
9087 ctx.close().await;
9088 mock_ctx.close().await;
9089 }
9090
9091 #[tokio::test(flavor = "multi_thread")]
9092 async fn test_edit_segments_persists_solver_feedback_for_next_mock_execute() {
9093 let initial_source = "\
9094sketch(on = XY) {
9095 line1 = line(start = [var 1, var 2], end = [var 1, var 2])
9096 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9097 fixed([line1.start, [0, 0]])
9098 coincident([line1.end, line2.start])
9099 equalLength([line1, line2])
9100}
9101";
9102
9103 let program = Program::parse(initial_source).unwrap().0.unwrap();
9104 let mut frontend = FrontendState::new();
9105 let mock_ctx = ExecutorContext::new_mock(None).await;
9106 let version = Version(0);
9107
9108 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9109 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9110 let sketch_id = sketch_object.id;
9111 let sketch = expect_sketch(sketch_object);
9112 let line2_end_id = *sketch.segments.get(4).unwrap();
9113
9114 let segments = vec![ExistingSegmentCtor {
9115 id: line2_end_id,
9116 ctor: SegmentCtor::Point(PointCtor {
9117 position: Point2d {
9118 x: Expr::Var(Number {
9119 value: 9.0,
9120 units: NumericSuffix::None,
9121 }),
9122 y: Expr::Var(Number {
9123 value: 10.0,
9124 units: NumericSuffix::None,
9125 }),
9126 },
9127 }),
9128 }];
9129 let (edited_source, _) = frontend
9130 .edit_segments(&mock_ctx, version, sketch_id, segments)
9131 .await
9132 .unwrap();
9133
9134 let (mock_source, _) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
9135 assert_eq!(mock_source.text, edited_source.text);
9136
9137 mock_ctx.close().await;
9138 }
9139
9140 #[tokio::test(flavor = "multi_thread")]
9143 async fn test_preview_edit_segments_does_not_persist_solver_feedback() {
9144 let initial_source = "\
9145sketch(on = XY) {
9146 line1 = line(start = [var 1, var 2], end = [var 1, var 2])
9147 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9148 fixed([line1.start, [0, 0]])
9149 coincident([line1.end, line2.start])
9150 equalLength([line1, line2])
9151}
9152";
9153
9154 let program = Program::parse(initial_source).unwrap().0.unwrap();
9155 let mut frontend = FrontendState::new();
9156 let mock_ctx = ExecutorContext::new_mock(None).await;
9157 let version = Version(0);
9158
9159 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9160 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9161 let sketch_id = sketch_object.id;
9162 let sketch = expect_sketch(sketch_object);
9163 let line2_end_id = *sketch.segments.get(4).unwrap();
9164
9165 let segments = vec![ExistingSegmentCtor {
9166 id: line2_end_id,
9167 ctor: SegmentCtor::Point(PointCtor {
9168 position: Point2d {
9169 x: Expr::Var(Number {
9170 value: 9.0,
9171 units: NumericSuffix::None,
9172 }),
9173 y: Expr::Var(Number {
9174 value: 10.0,
9175 units: NumericSuffix::None,
9176 }),
9177 },
9178 }),
9179 }];
9180 let (preview_source, preview_delta) = frontend
9181 .edit_segments_with_options(
9182 &mock_ctx,
9183 version,
9184 sketch_id,
9185 segments,
9186 EditSegmentsOptions {
9187 anchor_segment_ids: Some(vec![line2_end_id]),
9188 drag_anchors: Vec::new(),
9189 constraint_label_edits: Vec::new(),
9190 commit_solved_initial_guesses: false,
9191 },
9192 )
9193 .await
9194 .unwrap();
9195
9196 assert!(
9197 !preview_delta.exec_outcome.var_solutions.is_empty(),
9198 "preview solve should still solve and return geometry feedback"
9199 );
9200 assert!(
9201 preview_source
9202 .text
9203 .contains("line1 = line(start = [var 1, var 2], end = [var 1, var 2])")
9204 );
9205 assert!(
9206 preview_source
9207 .text
9208 .contains("line2 = line(start = [var 5, var 6], end = [var 9, var 10])")
9209 );
9210
9211 let (mock_source, _) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
9212 assert_eq!(mock_source.text, preview_source.text);
9213
9214 mock_ctx.close().await;
9215 }
9216
9217 #[tokio::test(flavor = "multi_thread")]
9218 async fn test_add_constraint_persists_solver_feedback_for_next_mock_execute() {
9219 let initial_source = "\
9220sketch(on = XY) {
9221 line1 = line(start = [var 0, var 0], end = [var 10, var 0])
9222}
9223";
9224
9225 let program = Program::parse(initial_source).unwrap().0.unwrap();
9226 let mut frontend = FrontendState::new();
9227 let mock_ctx = ExecutorContext::new_mock(None).await;
9228 let version = Version(0);
9229
9230 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9231 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9232 let sketch_id = sketch_object.id;
9233 let sketch = expect_sketch(sketch_object);
9234 let line_end_id = *sketch.segments.get(1).unwrap();
9235
9236 let constraint = Constraint::Fixed(Fixed {
9237 points: vec![FixedPoint {
9238 point: line_end_id,
9239 position: Point2d {
9240 x: Number {
9241 value: 20.0,
9242 units: NumericSuffix::Mm,
9243 },
9244 y: Number {
9245 value: 0.0,
9246 units: NumericSuffix::Mm,
9247 },
9248 },
9249 }],
9250 });
9251 let (constraint_source, _) = frontend
9252 .add_constraint(&mock_ctx, version, sketch_id, constraint)
9253 .await
9254 .unwrap();
9255
9256 assert!(
9257 constraint_source
9258 .text
9259 .contains("line1 = line(start = [var 0, var 0], end = [var 20, var 0])"),
9260 "{}",
9261 constraint_source.text
9262 );
9263 let (mock_source, _) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
9264 assert_eq!(mock_source.text, constraint_source.text);
9265
9266 mock_ctx.close().await;
9267 }
9268
9269 #[test]
9270 fn test_no_solver_feedback_preserves_original_source() {
9271 let initial_source = "\
9272@settings(defaultLengthUnit = in, kclVersion = 2.0)
9273cylinder = startSketchOn(XY)
9274 |> circle(center= [0, 0], radius= 22)
9275 |> extrude(length = 14)
9276";
9277 let mut frontend = FrontendState::new();
9278 frontend.program = Program::parse(initial_source).unwrap().0.unwrap();
9279 let outcome = ExecOutcome {
9280 variables: Default::default(),
9281 test_program_memory: Default::default(),
9282 operations: Default::default(),
9283 artifact_graph: Default::default(),
9284 scene_objects: Default::default(),
9285 source_range_to_object: Default::default(),
9286 var_solutions: Default::default(),
9287 refactor_metadata: Default::default(),
9288 issues: Default::default(),
9289 filenames: Default::default(),
9290 source_files: Default::default(),
9291 default_planes: Default::default(),
9292 };
9293
9294 let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9295
9296 assert_eq!(source_delta.text, initial_source);
9297 }
9298
9299 #[tokio::test(flavor = "multi_thread")]
9302 async fn test_edit_segments_with_anchor_ids_limits_drag_fixed_constraints() {
9303 let initial_source = "\
9304sketch(on = XY) {
9305 point1 = point(at = [var 0mm, var 0mm])
9306 point2 = point(at = [var 0mm, var 0mm])
9307 coincident([point1, point2])
9308}
9309";
9310
9311 let program = Program::parse(initial_source).unwrap().0.unwrap();
9312 let mut frontend = FrontendState::new();
9313 let mock_ctx = ExecutorContext::new_mock(None).await;
9314 let version = Version(0);
9315
9316 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9317 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9318 let sketch_id = sketch_object.id;
9319 let sketch = expect_sketch(sketch_object);
9320 let point1_id = sketch.segments[0];
9321 let point2_id = sketch.segments[1];
9322
9323 let segments = vec![
9324 ExistingSegmentCtor {
9325 id: point1_id,
9326 ctor: SegmentCtor::Point(PointCtor {
9327 position: point_expr_mm(10.0, 0.0),
9328 }),
9329 },
9330 ExistingSegmentCtor {
9331 id: point2_id,
9332 ctor: SegmentCtor::Point(PointCtor {
9333 position: point_expr_mm(100.0, 0.0),
9334 }),
9335 },
9336 ];
9337 let (_, scene_delta) = frontend
9338 .edit_segments_with_options(
9339 &mock_ctx,
9340 version,
9341 sketch_id,
9342 segments,
9343 EditSegmentsOptions {
9344 anchor_segment_ids: Some(vec![point1_id]),
9345 drag_anchors: Vec::new(),
9346 constraint_label_edits: Vec::new(),
9347 commit_solved_initial_guesses: true,
9348 },
9349 )
9350 .await
9351 .unwrap();
9352
9353 assert_point_position_close(
9354 point_position(&scene_delta.new_graph, point1_id),
9355 point_number_mm(10.0, 0.0),
9356 );
9357 assert_point_position_close(
9358 point_position(&scene_delta.new_graph, point2_id),
9359 point_number_mm(10.0, 0.0),
9360 );
9361
9362 mock_ctx.close().await;
9363 }
9364
9365 fn collect_sketch_var_literals_with_value(program: &Program, value: f64) -> Vec<(SourceRange, ast::NodePath)> {
9368 use std::cell::RefCell;
9369 struct Collector {
9370 target: f64,
9371 out: RefCell<Vec<(SourceRange, ast::NodePath)>>,
9372 }
9373 impl<'a> crate::walk::Visitor<'a> for &Collector {
9374 type Error = crate::front::Error;
9375 fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
9376 if let crate::walk::Node::SketchVar(sketch_var) = node
9377 && let (Some(initial), Some(node_path)) = (&sketch_var.initial, &sketch_var.node_path)
9378 && (initial.value - self.target).abs() < 1e-9
9379 {
9380 self.out
9381 .borrow_mut()
9382 .push((SourceRange::from(initial.as_ref()), node_path.clone()));
9383 }
9384 for child in node.children().iter() {
9385 if !child.visit(*self)? {
9386 return Ok(false);
9387 }
9388 }
9389 Ok(true)
9390 }
9391 }
9392 let collector = Collector {
9393 target: value,
9394 out: Default::default(),
9395 };
9396 let _ = crate::walk::Node::from(&program.ast).visit(&collector);
9397 collector.out.into_inner()
9398 }
9399
9400 fn collect_all_sketch_vars(program: &Program) -> Vec<(SourceRange, ast::NodePath)> {
9403 use std::cell::RefCell;
9404 struct Collector {
9405 out: RefCell<Vec<(SourceRange, ast::NodePath)>>,
9406 }
9407 impl<'a> crate::walk::Visitor<'a> for &Collector {
9408 type Error = crate::front::Error;
9409 fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
9410 if let crate::walk::Node::SketchVar(sketch_var) = node
9411 && let Some(node_path) = &sketch_var.node_path
9412 {
9413 self.out
9414 .borrow_mut()
9415 .push((SourceRange::from(sketch_var), node_path.clone()));
9416 }
9417 for child in node.children().iter() {
9418 if !child.visit(*self)? {
9419 return Ok(false);
9420 }
9421 }
9422 Ok(true)
9423 }
9424 }
9425 let collector = Collector {
9426 out: Default::default(),
9427 };
9428 let _ = crate::walk::Node::from(&program.ast).visit(&collector);
9429 collector.out.into_inner()
9430 }
9431
9432 fn empty_exec_outcome_with_var_solutions(
9433 var_solutions: Vec<(SourceRange, Option<ast::NodePath>, Number)>,
9434 ) -> ExecOutcome {
9435 ExecOutcome {
9436 variables: Default::default(),
9437 test_program_memory: Default::default(),
9438 operations: Default::default(),
9439 artifact_graph: Default::default(),
9440 scene_objects: Default::default(),
9441 source_range_to_object: Default::default(),
9442 var_solutions,
9443 refactor_metadata: Default::default(),
9444 issues: Default::default(),
9445 filenames: Default::default(),
9446 source_files: Default::default(),
9447 default_planes: Default::default(),
9448 }
9449 }
9450
9451 #[test]
9455 fn test_commit_var_solution_by_node_path_updates_sketch_var() {
9456 let initial_source = "\
9457sketch(on = XY) {
9458 line1 = line(start = [var 0, var 0], end = [var 10mm, var 0])
9459}
9460";
9461 let program = Program::parse(initial_source).unwrap().0.unwrap();
9462 let matches = collect_sketch_var_literals_with_value(&program, 10.0);
9463 assert_eq!(matches.len(), 1, "expected exactly one `var 10mm`");
9464 let (literal_range, node_path) = matches.into_iter().next().unwrap();
9465
9466 let mut frontend = FrontendState::new();
9467 frontend.program = program;
9468
9469 let outcome = empty_exec_outcome_with_var_solutions(vec![(
9470 literal_range,
9471 Some(node_path),
9472 Number {
9473 value: 25.0,
9474 units: NumericSuffix::Mm,
9475 },
9476 )]);
9477
9478 let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9479
9480 insta::assert_snapshot!(
9481 "test_commit_var_solution_by_node_path_updates_sketch_var",
9482 source_delta.text
9483 );
9484 }
9485
9486 #[test]
9493 fn test_commit_var_solution_survives_whitespace_shift_earlier_in_file() {
9494 let compact_source = "\
9495sketch(on = XY) {
9496 line1 = line(start = [var 0, var 0], end = [var 10mm, var 0])
9497}
9498";
9499 let padded_source = "\
9500// added comment\n// added comment\n\nsketch(on = XY) {
9501 line1 = line(start = [var 0, var 0], end = [var 10mm, var 0])
9502}
9503";
9504 let compact_program = Program::parse(compact_source).unwrap().0.unwrap();
9505 let padded_program = Program::parse(padded_source).unwrap().0.unwrap();
9506
9507 let compact_match = collect_sketch_var_literals_with_value(&compact_program, 10.0)
9508 .into_iter()
9509 .next()
9510 .expect("expected `var 10mm` in compact source");
9511 let padded_match = collect_sketch_var_literals_with_value(&padded_program, 10.0)
9512 .into_iter()
9513 .next()
9514 .expect("expected `var 10mm` in padded source");
9515
9516 assert_ne!(
9517 compact_match.0, padded_match.0,
9518 "byte offsets must differ for this test to be meaningful"
9519 );
9520 assert_eq!(
9521 compact_match.1, padded_match.1,
9522 "node paths must agree across whitespace; that's the whole point of NodePath",
9523 );
9524
9525 let mut frontend = FrontendState::new();
9526 frontend.program = padded_program;
9527
9528 let outcome = empty_exec_outcome_with_var_solutions(vec![(
9530 compact_match.0,
9531 Some(compact_match.1),
9532 Number {
9533 value: 30.0,
9534 units: NumericSuffix::Mm,
9535 },
9536 )]);
9537
9538 let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9539
9540 insta::assert_snapshot!(
9541 "test_commit_var_solution_survives_whitespace_shift_earlier_in_file",
9542 source_delta.text
9543 );
9544 }
9545
9546 #[test]
9550 fn test_commit_var_solution_node_path_wins_when_source_range_points_at_wrong_var() {
9551 let initial_source = "\
9552sketch(on = XY) {
9553 line1 = line(start = [var 10mm, var 0mm], end = [var 20mm, var 0mm])
9554}
9555";
9556 let program = Program::parse(initial_source).unwrap().0.unwrap();
9557
9558 let var_10 = collect_sketch_var_literals_with_value(&program, 10.0)
9559 .into_iter()
9560 .next()
9561 .expect("expected `var 10mm`");
9562 let var_20 = collect_sketch_var_literals_with_value(&program, 20.0)
9563 .into_iter()
9564 .next()
9565 .expect("expected `var 20mm`");
9566
9567 let mut frontend = FrontendState::new();
9568 frontend.program = program;
9569
9570 let outcome = empty_exec_outcome_with_var_solutions(vec![(
9572 var_20.0,
9573 Some(var_10.1),
9574 Number {
9575 value: 33.0,
9576 units: NumericSuffix::Mm,
9577 },
9578 )]);
9579
9580 let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9581
9582 insta::assert_snapshot!(
9583 "test_commit_var_solution_node_path_wins_when_source_range_points_at_wrong_var",
9584 source_delta.text
9585 );
9586 }
9587
9588 #[test]
9595 fn test_commit_var_solution_writes_back_into_bare_var() {
9596 let initial_source = "\
9597@settings(experimentalFeatures = allow, kclVersion = 2.0)
9598sketch(on = XY) {
9599 line1 = line(start = [var, var 0mm], end = [var 10mm, var 0])
9600}
9601";
9602 let program = Program::parse(initial_source).unwrap().0.unwrap();
9603
9604 let bare = collect_all_sketch_vars(&program)
9607 .into_iter()
9608 .find(|(range, _)| {
9609 range.end() - range.start() == 3
9611 })
9612 .expect("expected at least one bare `var`");
9613
9614 let mut frontend = FrontendState::new();
9615 frontend.program = program;
9616
9617 let outcome = empty_exec_outcome_with_var_solutions(vec![(
9618 bare.0,
9619 Some(bare.1),
9620 Number {
9621 value: 7.0,
9622 units: NumericSuffix::Mm,
9623 },
9624 )]);
9625
9626 let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9627
9628 insta::assert_snapshot!("test_commit_var_solution_writes_back_into_bare_var", source_delta.text);
9632 }
9633
9634 #[tokio::test(flavor = "multi_thread")]
9635 async fn test_delete_point_without_var() {
9636 let initial_source = "\
9637sketch(on = XY) {
9638 point(at = [var 1, var 2])
9639 point(at = [var 3, var 4])
9640 point(at = [var 5, var 6])
9641}
9642";
9643
9644 let program = Program::parse(initial_source).unwrap().0.unwrap();
9645
9646 let mut frontend = FrontendState::new();
9647
9648 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
9649 let mock_ctx = ExecutorContext::new_mock(None).await;
9650 let version = Version(0);
9651
9652 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9653 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9654 let sketch_id = sketch_object.id;
9655 let sketch = expect_sketch(sketch_object);
9656
9657 let point_id = *sketch.segments.get(1).unwrap();
9658
9659 let (src_delta, scene_delta) = frontend
9660 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
9661 .await
9662 .unwrap();
9663 insta::assert_snapshot!("test_delete_point_without_var", src_delta.text.as_str());
9664 assert_eq!(scene_delta.new_objects, vec![]);
9665 assert_eq!(scene_delta.new_graph.objects.len(), 4);
9666
9667 ctx.close().await;
9668 mock_ctx.close().await;
9669 }
9670
9671 #[tokio::test(flavor = "multi_thread")]
9672 async fn test_delete_point_with_var() {
9673 let initial_source = "\
9674sketch(on = XY) {
9675 point(at = [var 1, var 2])
9676 point1 = point(at = [var 3, var 4])
9677 point(at = [var 5, var 6])
9678}
9679";
9680
9681 let program = Program::parse(initial_source).unwrap().0.unwrap();
9682
9683 let mut frontend = FrontendState::new();
9684
9685 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
9686 let mock_ctx = ExecutorContext::new_mock(None).await;
9687 let version = Version(0);
9688
9689 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9690 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9691 let sketch_id = sketch_object.id;
9692 let sketch = expect_sketch(sketch_object);
9693
9694 let point_id = *sketch.segments.get(1).unwrap();
9695
9696 let (src_delta, scene_delta) = frontend
9697 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
9698 .await
9699 .unwrap();
9700 insta::assert_snapshot!("test_delete_point_with_var", src_delta.text.as_str());
9701 assert_eq!(scene_delta.new_objects, vec![]);
9702 assert_eq!(scene_delta.new_graph.objects.len(), 4);
9703
9704 ctx.close().await;
9705 mock_ctx.close().await;
9706 }
9707
9708 #[tokio::test(flavor = "multi_thread")]
9709 async fn test_delete_multiple_points() {
9710 let initial_source = "\
9711sketch(on = XY) {
9712 point(at = [var 1, var 2])
9713 point1 = point(at = [var 3, var 4])
9714 point(at = [var 5, var 6])
9715}
9716";
9717
9718 let program = Program::parse(initial_source).unwrap().0.unwrap();
9719
9720 let mut frontend = FrontendState::new();
9721
9722 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
9723 let mock_ctx = ExecutorContext::new_mock(None).await;
9724 let version = Version(0);
9725
9726 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9727 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9728 let sketch_id = sketch_object.id;
9729
9730 let sketch = expect_sketch(sketch_object);
9731
9732 let point1_id = *sketch.segments.first().unwrap();
9733 let point2_id = *sketch.segments.get(1).unwrap();
9734
9735 let (src_delta, scene_delta) = frontend
9736 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point1_id, point2_id])
9737 .await
9738 .unwrap();
9739 insta::assert_snapshot!("test_delete_multiple_points", src_delta.text.as_str());
9740 assert_eq!(scene_delta.new_objects, vec![]);
9741 assert_eq!(scene_delta.new_graph.objects.len(), 3);
9742
9743 ctx.close().await;
9744 mock_ctx.close().await;
9745 }
9746
9747 #[tokio::test(flavor = "multi_thread")]
9748 async fn test_delete_coincident_constraint() {
9749 let initial_source = "\
9750sketch(on = XY) {
9751 point1 = point(at = [var 1, var 2])
9752 point2 = point(at = [var 3, var 4])
9753 coincident([point1, point2])
9754 point(at = [var 5, var 6])
9755}
9756";
9757
9758 let program = Program::parse(initial_source).unwrap().0.unwrap();
9759
9760 let mut frontend = FrontendState::new();
9761
9762 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
9763 let mock_ctx = ExecutorContext::new_mock(None).await;
9764 let version = Version(0);
9765
9766 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9767 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9768 let sketch_id = sketch_object.id;
9769 let sketch = expect_sketch(sketch_object);
9770
9771 let coincident_id = *sketch.constraints.first().unwrap();
9772
9773 let (src_delta, scene_delta) = frontend
9774 .delete_objects(&mock_ctx, version, sketch_id, vec![coincident_id], Vec::new())
9775 .await
9776 .unwrap();
9777 insta::assert_snapshot!("test_delete_coincident_constraint", src_delta.text.as_str());
9778 assert_eq!(scene_delta.new_objects, vec![]);
9779 assert_eq!(scene_delta.new_graph.objects.len(), 5);
9780
9781 ctx.close().await;
9782 mock_ctx.close().await;
9783 }
9784
9785 #[tokio::test(flavor = "multi_thread")]
9786 async fn test_delete_line_cascades_to_coincident_constraint() {
9787 let initial_source = "\
9788sketch(on = XY) {
9789 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9790 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9791 coincident([line1.end, line2.start])
9792}
9793";
9794
9795 let program = Program::parse(initial_source).unwrap().0.unwrap();
9796
9797 let mut frontend = FrontendState::new();
9798
9799 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
9800 let mock_ctx = ExecutorContext::new_mock(None).await;
9801 let version = Version(0);
9802
9803 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9804 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9805 let sketch_id = sketch_object.id;
9806 let sketch = expect_sketch(sketch_object);
9807 let line_id = *sketch.segments.get(5).unwrap();
9808
9809 let (src_delta, scene_delta) = frontend
9810 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
9811 .await
9812 .unwrap();
9813 insta::assert_snapshot!(
9814 "test_delete_line_cascades_to_coincident_constraint",
9815 src_delta.text.as_str()
9816 );
9817 assert_eq!(
9818 scene_delta.new_graph.objects.len(),
9819 5,
9820 "{:#?}",
9821 scene_delta.new_graph.objects
9822 );
9823
9824 ctx.close().await;
9825 mock_ctx.close().await;
9826 }
9827
9828 #[tokio::test(flavor = "multi_thread")]
9829 async fn test_delete_line_cascades_to_distance_constraint() {
9830 let initial_source = "\
9831sketch(on = XY) {
9832 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9833 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9834 distance([line1.end, line2.start]) == 10mm
9835}
9836";
9837
9838 let program = Program::parse(initial_source).unwrap().0.unwrap();
9839
9840 let mut frontend = FrontendState::new();
9841
9842 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
9843 let mock_ctx = ExecutorContext::new_mock(None).await;
9844 let version = Version(0);
9845
9846 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9847 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9848 let sketch_id = sketch_object.id;
9849 let sketch = expect_sketch(sketch_object);
9850 let line_id = *sketch.segments.get(5).unwrap();
9851
9852 let (src_delta, scene_delta) = frontend
9853 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
9854 .await
9855 .unwrap();
9856 insta::assert_snapshot!(
9857 "test_delete_line_cascades_to_distance_constraint",
9858 src_delta.text.as_str()
9859 );
9860 assert_eq!(
9861 scene_delta.new_graph.objects.len(),
9862 5,
9863 "{:#?}",
9864 scene_delta.new_graph.objects
9865 );
9866
9867 ctx.close().await;
9868 mock_ctx.close().await;
9869 }
9870
9871 #[tokio::test(flavor = "multi_thread")]
9872 async fn test_delete_point_cascades_to_horizontal_distance_constraint() {
9873 let initial_source = "\
9874sketch(on = XY) {
9875 point1 = point(at = [var 1, var 2])
9876 point2 = point(at = [var 3, var 4])
9877 horizontalDistance([point1, point2]) == 10mm
9878}
9879";
9880
9881 let program = Program::parse(initial_source).unwrap().0.unwrap();
9882
9883 let mut frontend = FrontendState::new();
9884
9885 let mock_ctx = ExecutorContext::new_mock(None).await;
9886 let version = Version(0);
9887
9888 frontend.program = program.clone();
9889 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9890 frontend.update_state_after_exec(outcome, true);
9891 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9892 let sketch_id = sketch_object.id;
9893 let sketch = expect_sketch(sketch_object);
9894 let point2_id = *sketch.segments.get(1).unwrap();
9895
9896 let (src_delta, scene_delta) = frontend
9897 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point2_id])
9898 .await
9899 .unwrap();
9900 insta::assert_snapshot!(
9901 "test_delete_point_cascades_to_horizontal_distance_constraint",
9902 src_delta.text.as_str()
9903 );
9904 assert_eq!(
9905 scene_delta.new_graph.objects.len(),
9906 3,
9907 "{:#?}",
9908 scene_delta.new_graph.objects
9909 );
9910
9911 mock_ctx.close().await;
9912 }
9913
9914 #[tokio::test(flavor = "multi_thread")]
9915 async fn test_delete_line_cascades_to_fixed_constraint() {
9916 let initial_source = "\
9917sketch(on = XY) {
9918 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9919 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9920 fixed([line1.start, [0, 0]])
9921}
9922";
9923
9924 let program = Program::parse(initial_source).unwrap().0.unwrap();
9925
9926 let mut frontend = FrontendState::new();
9927
9928 let mock_ctx = ExecutorContext::new_mock(None).await;
9929 let version = Version(0);
9930
9931 frontend.program = program.clone();
9932 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9933 frontend.update_state_after_exec(outcome, true);
9934 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9935 let sketch_id = sketch_object.id;
9936 let sketch = expect_sketch(sketch_object);
9937 let line1_id = *sketch.segments.get(2).unwrap();
9938
9939 let (src_delta, scene_delta) = frontend
9940 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
9941 .await
9942 .unwrap();
9943 insta::assert_snapshot!("test_delete_line_cascades_to_fixed_constraint", src_delta.text.as_str());
9944 assert_eq!(
9945 scene_delta.new_graph.objects.len(),
9946 5,
9947 "{:#?}",
9948 scene_delta.new_graph.objects
9949 );
9950
9951 mock_ctx.close().await;
9952 }
9953
9954 #[tokio::test(flavor = "multi_thread")]
9955 async fn test_delete_line_cascades_to_midpoint_constraint() {
9956 let initial_source = "\
9957sketch(on = XY) {
9958 point1 = point(at = [var 1, var 2])
9959 line1 = line(start = [var 0, var 0], end = [var 6, var 4])
9960 midpoint(line1, point = point1)
9961}
9962";
9963
9964 let program = Program::parse(initial_source).unwrap().0.unwrap();
9965
9966 let mut frontend = FrontendState::new();
9967
9968 let mock_ctx = ExecutorContext::new_mock(None).await;
9969 let version = Version(0);
9970
9971 frontend.program = program.clone();
9972 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9973 frontend.update_state_after_exec(outcome, true);
9974 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9975 let sketch_id = sketch_object.id;
9976 let sketch = expect_sketch(sketch_object);
9977 let line1_id = *sketch.segments.get(3).unwrap();
9978
9979 let (src_delta, scene_delta) = frontend
9980 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
9981 .await
9982 .unwrap();
9983 insta::assert_snapshot!(
9984 "test_delete_line_cascades_to_midpoint_constraint",
9985 src_delta.text.as_str()
9986 );
9987 assert_eq!(
9988 scene_delta.new_graph.objects.len(),
9989 3,
9990 "{:#?}",
9991 scene_delta.new_graph.objects
9992 );
9993
9994 mock_ctx.close().await;
9995 }
9996
9997 #[tokio::test(flavor = "multi_thread")]
9998 async fn test_delete_point_preserves_multiline_coincident_constraint() {
9999 let initial_source = "\
10000sketch(on = XY) {
10001 point1 = point(at = [var 1, var 2])
10002 point2 = point(at = [var 3, var 4])
10003 point3 = point(at = [var 5, var 6])
10004 coincident([point1, point2, point3])
10005}
10006";
10007
10008 let program = Program::parse(initial_source).unwrap().0.unwrap();
10009
10010 let mut frontend = FrontendState::new();
10011
10012 let mock_ctx = ExecutorContext::new_mock(None).await;
10013 let version = Version(0);
10014
10015 frontend.program = program.clone();
10016 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10017 frontend.update_state_after_exec(outcome, true);
10018 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10019 let sketch_id = sketch_object.id;
10020 let sketch = expect_sketch(sketch_object);
10021 let point3_id = *sketch.segments.get(2).unwrap();
10022
10023 let (src_delta, scene_delta) = frontend
10024 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point3_id])
10025 .await
10026 .unwrap();
10027 assert!(src_delta.text.contains("point1 = point("), "{}", src_delta.text);
10028 assert!(src_delta.text.contains("point2 = point("), "{}", src_delta.text);
10029 assert!(!src_delta.text.contains("point3 = point("), "{}", src_delta.text);
10030 assert!(
10031 src_delta.text.contains("coincident([point1, point2])"),
10032 "{}",
10033 src_delta.text
10034 );
10035
10036 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10037 let sketch = expect_sketch(sketch_object);
10038 assert_eq!(sketch.segments.len(), 2);
10039 assert_eq!(sketch.constraints.len(), 1);
10040
10041 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10042 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10043 panic!("Expected constraint object");
10044 };
10045 let Constraint::Coincident(coincident) = constraint else {
10046 panic!("Expected coincident constraint");
10047 };
10048 assert_eq!(
10049 coincident.segments,
10050 sketch
10051 .segments
10052 .iter()
10053 .copied()
10054 .map(Into::into)
10055 .collect::<Vec<ConstraintSegment>>()
10056 );
10057
10058 mock_ctx.close().await;
10059 }
10060
10061 #[tokio::test(flavor = "multi_thread")]
10062 async fn test_delete_line_preserves_multiline_equal_length_constraint() {
10063 let initial_source = "\
10064sketch(on = XY) {
10065 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10066 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10067 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10068 equalLength([line1, line2, line3])
10069}
10070";
10071
10072 let program = Program::parse(initial_source).unwrap().0.unwrap();
10073
10074 let mut frontend = FrontendState::new();
10075
10076 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
10077 let mock_ctx = ExecutorContext::new_mock(None).await;
10078 let version = Version(0);
10079
10080 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10081 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10082 let sketch_id = sketch_object.id;
10083 let sketch = expect_sketch(sketch_object);
10084 let line3_id = *sketch.segments.get(8).unwrap();
10085
10086 let (src_delta, scene_delta) = frontend
10087 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line3_id])
10088 .await
10089 .unwrap();
10090 insta::assert_snapshot!(
10091 "test_delete_line_preserves_multiline_equal_length_constraint",
10092 src_delta.text.as_str()
10093 );
10094
10095 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10096 let sketch = expect_sketch(sketch_object);
10097 assert_eq!(sketch.constraints.len(), 1);
10098
10099 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10100 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10101 panic!("Expected constraint object");
10102 };
10103 let Constraint::LinesEqualLength(lines_equal_length) = constraint else {
10104 panic!("Expected lines equal length constraint");
10105 };
10106 assert_eq!(lines_equal_length.lines.len(), 2);
10107
10108 ctx.close().await;
10109 mock_ctx.close().await;
10110 }
10111
10112 #[tokio::test(flavor = "multi_thread")]
10113 async fn test_delete_line_preserves_multiline_horizontal_constraint() {
10114 let initial_source = "\
10115sketch(on = XY) {
10116 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10117 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10118 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10119 horizontal([line1.end, line2.start, line3.start])
10120}
10121";
10122
10123 let program = Program::parse(initial_source).unwrap().0.unwrap();
10124
10125 let mut frontend = FrontendState::new();
10126
10127 let mock_ctx = ExecutorContext::new_mock(None).await;
10128 let version = Version(0);
10129
10130 frontend.program = program.clone();
10131 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10132 frontend.update_state_after_exec(outcome, true);
10133 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10134 let sketch_id = sketch_object.id;
10135 let sketch = expect_sketch(sketch_object);
10136 let line1_id = *sketch.segments.get(2).unwrap();
10137
10138 let (src_delta, scene_delta) = frontend
10139 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
10140 .await
10141 .unwrap();
10142 assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
10143 assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
10144 assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
10145 assert!(
10146 src_delta.text.contains("horizontal([line2.start, line3.start])"),
10147 "{}",
10148 src_delta.text
10149 );
10150
10151 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10152 let sketch = expect_sketch(sketch_object);
10153 assert_eq!(sketch.constraints.len(), 1);
10154
10155 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10156 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10157 panic!("Expected constraint object");
10158 };
10159 let Constraint::Horizontal(Horizontal::Points { points }) = constraint else {
10160 panic!("Expected horizontal points constraint");
10161 };
10162 let remaining_points = vec![sketch.segments[0].into(), sketch.segments[3].into()];
10163 assert_eq!(*points, remaining_points);
10164
10165 mock_ctx.close().await;
10166 }
10167
10168 #[tokio::test(flavor = "multi_thread")]
10169 async fn test_delete_line_preserves_multiline_vertical_constraint() {
10170 let initial_source = "\
10171sketch(on = XY) {
10172 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10173 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10174 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10175 vertical([line1.end, line2.start, line3.start])
10176}
10177";
10178
10179 let program = Program::parse(initial_source).unwrap().0.unwrap();
10180
10181 let mut frontend = FrontendState::new();
10182
10183 let mock_ctx = ExecutorContext::new_mock(None).await;
10184 let version = Version(0);
10185
10186 frontend.program = program.clone();
10187 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10188 frontend.update_state_after_exec(outcome, true);
10189 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10190 let sketch_id = sketch_object.id;
10191 let sketch = expect_sketch(sketch_object);
10192 let line1_id = *sketch.segments.get(2).unwrap();
10193
10194 let (src_delta, scene_delta) = frontend
10195 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
10196 .await
10197 .unwrap();
10198 assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
10199 assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
10200 assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
10201 assert!(
10202 src_delta.text.contains("vertical([line2.start, line3.start])"),
10203 "{}",
10204 src_delta.text
10205 );
10206
10207 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10208 let sketch = expect_sketch(sketch_object);
10209 assert_eq!(sketch.constraints.len(), 1);
10210
10211 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10212 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10213 panic!("Expected constraint object");
10214 };
10215 let Constraint::Vertical(Vertical::Points { points }) = constraint else {
10216 panic!("Expected vertical points constraint");
10217 };
10218 let remaining_points = vec![sketch.segments[0].into(), sketch.segments[3].into()];
10219 assert_eq!(*points, remaining_points);
10220
10221 mock_ctx.close().await;
10222 }
10223
10224 #[tokio::test(flavor = "multi_thread")]
10225 async fn test_delete_line_preserves_multiline_coincident_constraint() {
10226 let initial_source = "\
10227sketch(on = XY) {
10228 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10229 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10230 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10231 coincident([line1.end, line2.start, line3.start])
10232}
10233";
10234
10235 let program = Program::parse(initial_source).unwrap().0.unwrap();
10236
10237 let mut frontend = FrontendState::new();
10238
10239 let mock_ctx = ExecutorContext::new_mock(None).await;
10240 let version = Version(0);
10241
10242 frontend.program = program.clone();
10243 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10244 frontend.update_state_after_exec(outcome, true);
10245 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10246 let sketch_id = sketch_object.id;
10247 let sketch = expect_sketch(sketch_object);
10248 let line1_id = *sketch.segments.get(2).unwrap();
10249
10250 let (src_delta, scene_delta) = frontend
10251 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
10252 .await
10253 .unwrap();
10254 assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
10255 assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
10256 assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
10257 assert!(
10258 src_delta.text.contains("coincident([line2.start, line3.start])"),
10259 "{}",
10260 src_delta.text
10261 );
10262
10263 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10264 let sketch = expect_sketch(sketch_object);
10265 assert_eq!(sketch.constraints.len(), 1);
10266
10267 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10268 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10269 panic!("Expected constraint object");
10270 };
10271 let Constraint::Coincident(coincident) = constraint else {
10272 panic!("Expected coincident constraint");
10273 };
10274 let remaining_segments = vec![sketch.segments[0].into(), sketch.segments[3].into()];
10275 assert_eq!(coincident.segments, remaining_segments);
10276
10277 mock_ctx.close().await;
10278 }
10279
10280 #[tokio::test(flavor = "multi_thread")]
10281 async fn test_delete_lines_removes_multiline_equal_length_constraint_below_minimum() {
10282 let initial_source = "\
10283sketch(on = XY) {
10284 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10285 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10286 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10287 equalLength([line1, line2, line3])
10288}
10289";
10290
10291 let program = Program::parse(initial_source).unwrap().0.unwrap();
10292
10293 let mut frontend = FrontendState::new();
10294
10295 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
10296 let mock_ctx = ExecutorContext::new_mock(None).await;
10297 let version = Version(0);
10298
10299 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10300 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10301 let sketch_id = sketch_object.id;
10302 let sketch = expect_sketch(sketch_object);
10303 let line2_id = *sketch.segments.get(5).unwrap();
10304 let line3_id = *sketch.segments.get(8).unwrap();
10305
10306 let (src_delta, scene_delta) = frontend
10307 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line2_id, line3_id])
10308 .await
10309 .unwrap();
10310 insta::assert_snapshot!(
10311 "test_delete_lines_removes_multiline_equal_length_constraint_below_minimum",
10312 src_delta.text.as_str()
10313 );
10314
10315 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10316 let sketch = expect_sketch(sketch_object);
10317 assert!(sketch.constraints.is_empty());
10318
10319 ctx.close().await;
10320 mock_ctx.close().await;
10321 }
10322
10323 #[tokio::test(flavor = "multi_thread")]
10324 async fn test_delete_line_preserves_multiline_parallel_constraint() {
10325 let initial_source = "\
10326sketch(on = XY) {
10327 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10328 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10329 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10330 parallel([line1, line2, line3])
10331}
10332";
10333
10334 let program = Program::parse(initial_source).unwrap().0.unwrap();
10335
10336 let mut frontend = FrontendState::new();
10337
10338 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
10339 let mock_ctx = ExecutorContext::new_mock(None).await;
10340 let version = Version(0);
10341
10342 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10343 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10344 let sketch_id = sketch_object.id;
10345 let sketch = expect_sketch(sketch_object);
10346 let line3_id = *sketch.segments.get(8).unwrap();
10347
10348 let (src_delta, scene_delta) = frontend
10349 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line3_id])
10350 .await
10351 .unwrap();
10352 insta::assert_snapshot!(
10353 "test_delete_line_preserves_multiline_parallel_constraint",
10354 src_delta.text.as_str()
10355 );
10356
10357 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10358 let sketch = expect_sketch(sketch_object);
10359 assert_eq!(sketch.constraints.len(), 1);
10360
10361 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10362 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10363 panic!("Expected constraint object");
10364 };
10365 let Constraint::Parallel(parallel) = constraint else {
10366 panic!("Expected parallel constraint");
10367 };
10368 assert_eq!(parallel.lines.len(), 2);
10369
10370 ctx.close().await;
10371 mock_ctx.close().await;
10372 }
10373
10374 #[tokio::test(flavor = "multi_thread")]
10375 async fn test_delete_lines_removes_multiline_parallel_constraint_below_minimum() {
10376 let initial_source = "\
10377sketch(on = XY) {
10378 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10379 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10380 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10381 parallel([line1, line2, line3])
10382}
10383";
10384
10385 let program = Program::parse(initial_source).unwrap().0.unwrap();
10386
10387 let mut frontend = FrontendState::new();
10388
10389 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
10390 let mock_ctx = ExecutorContext::new_mock(None).await;
10391 let version = Version(0);
10392
10393 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10394 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10395 let sketch_id = sketch_object.id;
10396 let sketch = expect_sketch(sketch_object);
10397 let line2_id = *sketch.segments.get(5).unwrap();
10398 let line3_id = *sketch.segments.get(8).unwrap();
10399
10400 let (src_delta, scene_delta) = frontend
10401 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line2_id, line3_id])
10402 .await
10403 .unwrap();
10404 insta::assert_snapshot!(
10405 "test_delete_lines_removes_multiline_parallel_constraint_below_minimum",
10406 src_delta.text.as_str()
10407 );
10408
10409 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10410 let sketch = expect_sketch(sketch_object);
10411 assert!(sketch.constraints.is_empty());
10412
10413 ctx.close().await;
10414 mock_ctx.close().await;
10415 }
10416
10417 #[tokio::test(flavor = "multi_thread")]
10418 async fn test_delete_line_line_coincident_constraint() {
10419 let initial_source = "\
10420sketch(on = XY) {
10421 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10422 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10423 coincident([line1, line2])
10424}
10425";
10426
10427 let program = Program::parse(initial_source).unwrap().0.unwrap();
10428
10429 let mut frontend = FrontendState::new();
10430
10431 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
10432 let mock_ctx = ExecutorContext::new_mock(None).await;
10433 let version = Version(0);
10434
10435 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10436 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10437 let sketch_id = sketch_object.id;
10438 let sketch = expect_sketch(sketch_object);
10439
10440 let coincident_id = *sketch.constraints.first().unwrap();
10441
10442 let (src_delta, scene_delta) = frontend
10443 .delete_objects(&mock_ctx, version, sketch_id, vec![coincident_id], Vec::new())
10444 .await
10445 .unwrap();
10446 insta::assert_snapshot!("test_delete_line_line_coincident_constraint", src_delta.text.as_str());
10447 assert_eq!(scene_delta.new_objects, vec![]);
10448 assert_eq!(scene_delta.new_graph.objects.len(), 8);
10449
10450 ctx.close().await;
10451 mock_ctx.close().await;
10452 }
10453
10454 #[tokio::test(flavor = "multi_thread")]
10455 async fn test_two_points_coincident() {
10456 let initial_source = "\
10457sketch(on = XY) {
10458 point1 = point(at = [var 1, var 2])
10459 point(at = [3, 4])
10460}
10461";
10462
10463 let program = Program::parse(initial_source).unwrap().0.unwrap();
10464
10465 let mut frontend = FrontendState::new();
10466
10467 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
10468 let mock_ctx = ExecutorContext::new_mock(None).await;
10469 let version = Version(0);
10470
10471 frontend.hack_set_program(&ctx, program).await.unwrap();
10472 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10473 let sketch_id = sketch_object.id;
10474 let sketch = expect_sketch(sketch_object);
10475 let point0_id = *sketch.segments.first().unwrap();
10476 let point1_id = *sketch.segments.get(1).unwrap();
10477
10478 let constraint = Constraint::Coincident(Coincident {
10479 segments: vec![point0_id.into(), point1_id.into()],
10480 });
10481 let (src_delta, scene_delta) = frontend
10482 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10483 .await
10484 .unwrap();
10485 insta::assert_snapshot!("test_two_points_coincident", src_delta.text.as_str());
10486 assert_eq!(
10487 scene_delta.new_graph.objects.len(),
10488 5,
10489 "{:#?}",
10490 scene_delta.new_graph.objects
10491 );
10492
10493 ctx.close().await;
10494 mock_ctx.close().await;
10495 }
10496
10497 #[tokio::test(flavor = "multi_thread")]
10498 async fn test_three_points_coincident() {
10499 let initial_source = "\
10500sketch(on = XY) {
10501 point1 = point(at = [var 1, var 2])
10502 point(at = [var 3, var 4])
10503 point(at = [var 5, var 6])
10504}
10505";
10506
10507 let program = Program::parse(initial_source).unwrap().0.unwrap();
10508
10509 let mut frontend = FrontendState::new();
10510
10511 let mock_ctx = ExecutorContext::new_mock(None).await;
10512 let version = Version(0);
10513
10514 frontend.program = program.clone();
10515 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10516 frontend.update_state_after_exec(outcome, true);
10517 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10518 let sketch_id = sketch_object.id;
10519 let sketch = expect_sketch(sketch_object);
10520 let segments = sketch
10521 .segments
10522 .iter()
10523 .take(3)
10524 .copied()
10525 .map(Into::into)
10526 .collect::<Vec<ConstraintSegment>>();
10527
10528 let constraint = Constraint::Coincident(Coincident {
10529 segments: segments.clone(),
10530 });
10531 let (src_delta, scene_delta) = frontend
10532 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10533 .await
10534 .unwrap();
10535 insta::assert_snapshot!("test_three_points_coincident", src_delta.text.as_str());
10536
10537 let constraint_object = scene_delta
10538 .new_graph
10539 .objects
10540 .iter()
10541 .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
10542 .unwrap();
10543
10544 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10545 panic!("expected a constraint object");
10546 };
10547
10548 assert_eq!(constraint, &Constraint::Coincident(Coincident { segments }));
10549
10550 mock_ctx.close().await;
10551 }
10552
10553 #[tokio::test(flavor = "multi_thread")]
10554 async fn test_source_with_three_point_coincident_tracks_all_segments() {
10555 let initial_source = "\
10556sketch(on = XY) {
10557 point1 = point(at = [var 1, var 2])
10558 point2 = point(at = [var 3, var 4])
10559 point3 = point(at = [var 5, var 6])
10560 coincident([point1, point2, point3])
10561}
10562";
10563
10564 let program = Program::parse(initial_source).unwrap().0.unwrap();
10565
10566 let mut frontend = FrontendState::new();
10567
10568 let ctx = ExecutorContext::new_mock(None).await;
10569 frontend.program = program.clone();
10570 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10571 frontend.update_state_after_exec(outcome, true);
10572
10573 let constraint_object = frontend
10574 .scene_graph
10575 .objects
10576 .iter()
10577 .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
10578 .unwrap();
10579 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10580 panic!("expected a constraint object");
10581 };
10582
10583 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10584 let sketch = expect_sketch(sketch_object);
10585 let expected_segments = sketch
10586 .segments
10587 .iter()
10588 .take(3)
10589 .copied()
10590 .map(Into::into)
10591 .collect::<Vec<ConstraintSegment>>();
10592
10593 assert_eq!(
10594 constraint,
10595 &Constraint::Coincident(Coincident {
10596 segments: expected_segments,
10597 })
10598 );
10599
10600 ctx.close().await;
10601 }
10602
10603 #[tokio::test(flavor = "multi_thread")]
10604 async fn test_point_origin_coincident_preserves_order() {
10605 let initial_source = "\
10606sketch(on = XY) {
10607 point(at = [var 1, var 2])
10608}
10609";
10610
10611 for (origin_first, snapshot_name) in [
10612 (true, "test_point_origin_coincident_preserves_order_origin_first"),
10613 (false, "test_point_origin_coincident_preserves_order_point_first"),
10614 ] {
10615 let program = Program::parse(initial_source).unwrap().0.unwrap();
10616
10617 let mut frontend = FrontendState::new();
10618
10619 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
10620 let mock_ctx = ExecutorContext::new_mock(None).await;
10621 let version = Version(0);
10622
10623 frontend.hack_set_program(&ctx, program).await.unwrap();
10624 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10625 let sketch_id = sketch_object.id;
10626 let sketch = expect_sketch(sketch_object);
10627 let point_id = *sketch.segments.first().unwrap();
10628
10629 let segments = if origin_first {
10630 vec![ConstraintSegment::ORIGIN, point_id.into()]
10631 } else {
10632 vec![point_id.into(), ConstraintSegment::ORIGIN]
10633 };
10634 let constraint = Constraint::Coincident(Coincident {
10635 segments: segments.clone(),
10636 });
10637 let (src_delta, scene_delta) = frontend
10638 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10639 .await
10640 .unwrap();
10641 insta::assert_snapshot!(snapshot_name, src_delta.text.as_str());
10642
10643 let constraint_object = scene_delta
10644 .new_graph
10645 .objects
10646 .iter()
10647 .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
10648 .unwrap();
10649
10650 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10651 panic!("expected a constraint object");
10652 };
10653
10654 assert_eq!(constraint, &Constraint::Coincident(Coincident { segments }));
10655
10656 ctx.close().await;
10657 mock_ctx.close().await;
10658 }
10659 }
10660
10661 #[tokio::test(flavor = "multi_thread")]
10662 async fn test_coincident_of_line_end_points() {
10663 let initial_source = "\
10664sketch(on = XY) {
10665 line(start = [var 1, var 2], end = [var 3, var 4])
10666 line(start = [var 5, var 6], end = [var 7, var 8])
10667}
10668";
10669
10670 let program = Program::parse(initial_source).unwrap().0.unwrap();
10671
10672 let mut frontend = FrontendState::new();
10673
10674 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
10675 let mock_ctx = ExecutorContext::new_mock(None).await;
10676 let version = Version(0);
10677
10678 frontend.hack_set_program(&ctx, program).await.unwrap();
10679 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10680 let sketch_id = sketch_object.id;
10681 let sketch = expect_sketch(sketch_object);
10682 let point0_id = *sketch.segments.get(1).unwrap();
10683 let point1_id = *sketch.segments.get(3).unwrap();
10684
10685 let constraint = Constraint::Coincident(Coincident {
10686 segments: vec![point0_id.into(), point1_id.into()],
10687 });
10688 let (src_delta, scene_delta) = frontend
10689 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10690 .await
10691 .unwrap();
10692 insta::assert_snapshot!("test_coincident_of_line_end_points", src_delta.text.as_str());
10693 assert_eq!(
10694 scene_delta.new_graph.objects.len(),
10695 9,
10696 "{:#?}",
10697 scene_delta.new_graph.objects
10698 );
10699
10700 ctx.close().await;
10701 mock_ctx.close().await;
10702 }
10703
10704 #[tokio::test(flavor = "multi_thread")]
10705 async fn test_coincident_of_line_point_and_circle_segment() {
10706 let initial_source = "\
10707sketch(on = XY) {
10708 circle1 = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
10709 line1 = line(start = [var 9mm, var 1mm], end = [var 10mm, var 2mm])
10710}
10711";
10712 let program = Program::parse(initial_source).unwrap().0.unwrap();
10713 let mut frontend = FrontendState::new();
10714
10715 let mock_ctx = ExecutorContext::new_mock(None).await;
10716 let version = Version(0);
10717
10718 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10719 frontend.program = program;
10720 frontend.update_state_after_exec(outcome, true);
10721 let sketch_object = find_first_sketch_object(&frontend.scene_graph).expect("Expected sketch object");
10722 let sketch_id = sketch_object.id;
10723 let sketch = expect_sketch(sketch_object);
10724
10725 let circle_id = sketch
10726 .segments
10727 .iter()
10728 .copied()
10729 .find(|seg_id| {
10730 matches!(
10731 &frontend.scene_graph.objects[seg_id.0].kind,
10732 ObjectKind::Segment {
10733 segment: Segment::Circle(_)
10734 }
10735 )
10736 })
10737 .expect("Expected a circle segment in sketch");
10738 let line_id = frontend
10739 .scene_graph
10740 .objects
10741 .iter()
10742 .find_map(|obj| match &obj.kind {
10743 ObjectKind::Segment {
10744 segment: Segment::Line(line),
10745 } if line.owner.is_none() => Some(obj.id),
10746 _ => None,
10747 })
10748 .expect("Expected a standalone line segment in scene graph");
10749
10750 let line_start_point_id = match &frontend.scene_graph.objects[line_id.0].kind {
10751 ObjectKind::Segment {
10752 segment: Segment::Line(line),
10753 } => line.start,
10754 _ => panic!("Expected line segment object"),
10755 };
10756
10757 let constraint = Constraint::Coincident(Coincident {
10758 segments: vec![line_start_point_id.into(), circle_id.into()],
10759 });
10760 let (src_delta, _scene_delta) = frontend
10761 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10762 .await
10763 .unwrap();
10764 insta::assert_snapshot!(
10765 "test_coincident_of_line_point_and_circle_segment",
10766 src_delta.text.as_str()
10767 );
10768
10769 mock_ctx.close().await;
10770 }
10771
10772 #[tokio::test(flavor = "multi_thread")]
10773 async fn test_invalid_coincident_arc_and_line_preserves_state() {
10774 let program = Program::empty();
10782
10783 let mut frontend = FrontendState::new();
10784 frontend.program = program;
10785
10786 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
10787 let mock_ctx = ExecutorContext::new_mock(None).await;
10788 let version = Version(0);
10789
10790 let sketch_args = SketchCtor {
10791 on: Plane::Default(PlaneName::Xy),
10792 };
10793 let (_src_delta, _scene_delta, sketch_id) = frontend
10794 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
10795 .await
10796 .unwrap();
10797
10798 let arc_ctor = ArcCtor {
10800 start: Point2d {
10801 x: Expr::Var(Number {
10802 value: 0.0,
10803 units: NumericSuffix::Mm,
10804 }),
10805 y: Expr::Var(Number {
10806 value: 0.0,
10807 units: NumericSuffix::Mm,
10808 }),
10809 },
10810 end: Point2d {
10811 x: Expr::Var(Number {
10812 value: 10.0,
10813 units: NumericSuffix::Mm,
10814 }),
10815 y: Expr::Var(Number {
10816 value: 10.0,
10817 units: NumericSuffix::Mm,
10818 }),
10819 },
10820 center: Point2d {
10821 x: Expr::Var(Number {
10822 value: 10.0,
10823 units: NumericSuffix::Mm,
10824 }),
10825 y: Expr::Var(Number {
10826 value: 0.0,
10827 units: NumericSuffix::Mm,
10828 }),
10829 },
10830 direction: None,
10831 construction: None,
10832 };
10833 let (_src_delta, scene_delta) = frontend
10834 .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Arc(arc_ctor), None)
10835 .await
10836 .unwrap();
10837 let arc_id = *scene_delta.new_objects.last().unwrap();
10839
10840 let line_ctor = LineCtor {
10842 start: Point2d {
10843 x: Expr::Var(Number {
10844 value: 20.0,
10845 units: NumericSuffix::Mm,
10846 }),
10847 y: Expr::Var(Number {
10848 value: 0.0,
10849 units: NumericSuffix::Mm,
10850 }),
10851 },
10852 end: Point2d {
10853 x: Expr::Var(Number {
10854 value: 30.0,
10855 units: NumericSuffix::Mm,
10856 }),
10857 y: Expr::Var(Number {
10858 value: 10.0,
10859 units: NumericSuffix::Mm,
10860 }),
10861 },
10862 construction: None,
10863 };
10864 let (_src_delta, scene_delta) = frontend
10865 .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Line(line_ctor), None)
10866 .await
10867 .unwrap();
10868 let line_id = *scene_delta.new_objects.last().unwrap();
10870
10871 let constraint = Constraint::Coincident(Coincident {
10874 segments: vec![arc_id.into(), line_id.into()],
10875 });
10876 let result = frontend.add_constraint(&mock_ctx, version, sketch_id, constraint).await;
10877
10878 assert!(result.is_err(), "Expected invalid coincident constraint to fail");
10880
10881 let sketch_object_after =
10884 find_first_sketch_object(&frontend.scene_graph).expect("Sketch should still exist after failed constraint");
10885 let sketch_after = expect_sketch(sketch_object_after);
10886
10887 assert!(
10889 sketch_after.segments.contains(&arc_id),
10890 "Arc segment should still exist after failed constraint"
10891 );
10892 assert!(
10893 sketch_after.segments.contains(&line_id),
10894 "Line segment should still exist after failed constraint"
10895 );
10896
10897 let arc_obj = frontend
10899 .scene_graph
10900 .objects
10901 .get(arc_id.0)
10902 .expect("Arc object should still be accessible");
10903 let line_obj = frontend
10904 .scene_graph
10905 .objects
10906 .get(line_id.0)
10907 .expect("Line object should still be accessible");
10908
10909 match &arc_obj.kind {
10912 ObjectKind::Segment {
10913 segment: Segment::Arc(_),
10914 } => {}
10915 _ => panic!("Arc object should still be an arc segment"),
10916 }
10917 match &line_obj.kind {
10918 ObjectKind::Segment {
10919 segment: Segment::Line(_),
10920 } => {}
10921 _ => panic!("Line object should still be a line segment"),
10922 }
10923
10924 ctx.close().await;
10925 mock_ctx.close().await;
10926 }
10927
10928 #[tokio::test(flavor = "multi_thread")]
10929 async fn test_distance_two_points() {
10930 let initial_source = "\
10931sketch(on = XY) {
10932 point(at = [var 1, var 2])
10933 point(at = [var 3, var 4])
10934}
10935";
10936
10937 let program = Program::parse(initial_source).unwrap().0.unwrap();
10938
10939 let mut frontend = FrontendState::new();
10940
10941 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
10942 let mock_ctx = ExecutorContext::new_mock(None).await;
10943 let version = Version(0);
10944
10945 frontend.hack_set_program(&ctx, program).await.unwrap();
10946 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10947 let sketch_id = sketch_object.id;
10948 let sketch = expect_sketch(sketch_object);
10949 let point0_id = *sketch.segments.first().unwrap();
10950 let point1_id = *sketch.segments.get(1).unwrap();
10951
10952 let constraint = Constraint::Distance(Distance {
10953 segments: vec![point0_id.into(), point1_id.into()],
10954 distance: Number {
10955 value: 2.0,
10956 units: NumericSuffix::Mm,
10957 },
10958 label_position: None,
10959 source: Default::default(),
10960 });
10961 let (src_delta, scene_delta) = frontend
10962 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10963 .await
10964 .unwrap();
10965 insta::assert_snapshot!("test_distance_two_points", src_delta.text.as_str());
10966 assert_eq!(
10967 scene_delta.new_graph.objects.len(),
10968 5,
10969 "{:#?}",
10970 scene_delta.new_graph.objects
10971 );
10972
10973 ctx.close().await;
10974 mock_ctx.close().await;
10975 }
10976
10977 #[tokio::test(flavor = "multi_thread")]
10978 async fn test_distance_two_points_with_label() {
10979 let initial_source = "\
10980sketch(on = XY) {
10981 point(at = [var 1, var 2])
10982 point(at = [var 3, var 4])
10983}
10984";
10985
10986 let program = Program::parse(initial_source).unwrap().0.unwrap();
10987
10988 let mut frontend = FrontendState::new();
10989
10990 let mock_ctx = ExecutorContext::new_mock(None).await;
10991 let version = Version(0);
10992
10993 frontend.program = program.clone();
10994 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10995 frontend.update_state_after_exec(outcome, true);
10996 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10997 let sketch_id = sketch_object.id;
10998 let sketch = expect_sketch(sketch_object);
10999 let point0_id = *sketch.segments.first().unwrap();
11000 let point1_id = *sketch.segments.get(1).unwrap();
11001
11002 let label_position = Point2d {
11003 x: Number {
11004 value: 10.0,
11005 units: NumericSuffix::Mm,
11006 },
11007 y: Number {
11008 value: 11.0,
11009 units: NumericSuffix::Mm,
11010 },
11011 };
11012 let constraint = Constraint::Distance(Distance {
11013 segments: vec![point0_id.into(), point1_id.into()],
11014 distance: Number {
11015 value: 2.0,
11016 units: NumericSuffix::Mm,
11017 },
11018 label_position: Some(label_position.clone()),
11019 source: Default::default(),
11020 });
11021 let (src_delta, scene_delta) = frontend
11022 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11023 .await
11024 .unwrap();
11025 insta::assert_snapshot!("test_distance_two_points_with_label", src_delta.text.as_str());
11026
11027 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
11028 let sketch = expect_sketch(sketch_object);
11029 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
11030 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11031 panic!("Expected constraint object");
11032 };
11033 let Constraint::Distance(distance) = constraint else {
11034 panic!("Expected distance constraint");
11035 };
11036 assert_eq!(distance.label_position, Some(label_position));
11037
11038 mock_ctx.close().await;
11039 }
11040
11041 #[tokio::test(flavor = "multi_thread")]
11042 async fn test_edit_distance_constraint_label_position() {
11043 let initial_source = "\
11044sketch(on = XY) {
11045 point(at = [var 1, var 2])
11046 point(at = [var 3, var 2])
11047}
11048";
11049
11050 let program = Program::parse(initial_source).unwrap().0.unwrap();
11051
11052 let mut frontend = FrontendState::new();
11053
11054 let mock_ctx = ExecutorContext::new_mock(None).await;
11055 let version = Version(0);
11056
11057 frontend.program = program.clone();
11058 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11059 frontend.update_state_after_exec(outcome, true);
11060 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11061 let sketch_id = sketch_object.id;
11062 let sketch = expect_sketch(sketch_object);
11063 let point0_id = *sketch.segments.first().unwrap();
11064 let point1_id = *sketch.segments.get(1).unwrap();
11065
11066 let constraint = Constraint::Distance(Distance {
11067 segments: vec![point0_id.into(), point1_id.into()],
11068 distance: Number {
11069 value: 2.0,
11070 units: NumericSuffix::Mm,
11071 },
11072 label_position: None,
11073 source: Default::default(),
11074 });
11075 let (_, scene_delta) = frontend
11076 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11077 .await
11078 .unwrap();
11079 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
11080 let sketch = expect_sketch(sketch_object);
11081 let constraint_id = sketch.constraints[0];
11082 let label_position = Point2d {
11083 x: Number {
11084 value: 10.0,
11085 units: NumericSuffix::Mm,
11086 },
11087 y: Number {
11088 value: 11.0,
11089 units: NumericSuffix::Mm,
11090 },
11091 };
11092
11093 let (src_delta, scene_delta) = frontend
11094 .edit_distance_constraint_label_position(
11095 &mock_ctx,
11096 version,
11097 sketch_id,
11098 constraint_id,
11099 label_position.clone(),
11100 vec![],
11101 )
11102 .await
11103 .unwrap();
11104 insta::assert_snapshot!("test_edit_distance_constraint_label_position", src_delta.text.as_str());
11105
11106 let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
11107 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11108 panic!("Expected constraint object");
11109 };
11110 let Constraint::Distance(distance) = constraint else {
11111 panic!("Expected distance constraint");
11112 };
11113 assert_eq!(distance.label_position, Some(label_position));
11114
11115 mock_ctx.close().await;
11116 }
11117
11118 #[tokio::test(flavor = "multi_thread")]
11119 async fn test_edit_distance_constraint_type_and_value() {
11120 let initial_source = "\
11121sketch(on = XY) {
11122 line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 3mm])
11123 distance([line1.start, line1.end]) == 5mm
11124}
11125";
11126
11127 let program = Program::parse(initial_source).unwrap().0.unwrap();
11128 let mut frontend = FrontendState::new();
11129 let mock_ctx = ExecutorContext::new_mock(None).await;
11130 let version = Version(0);
11131
11132 frontend.program = program.clone();
11133 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11134 frontend.update_state_after_exec(outcome, true);
11135 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11136 let sketch_id = sketch_object.id;
11137 let sketch = expect_sketch(sketch_object);
11138 let constraint_id = sketch.constraints[0];
11139 let point0_id = sketch.segments[0];
11140 let point1_id = sketch.segments[1];
11141 let label_position = Point2d {
11142 x: Number {
11143 value: 2.0,
11144 units: NumericSuffix::Mm,
11145 },
11146 y: Number {
11147 value: 5.0,
11148 units: NumericSuffix::Mm,
11149 },
11150 };
11151
11152 let (source_delta, scene_delta) = frontend
11153 .edit_distance_constraint_with_options(
11154 &mock_ctx,
11155 version,
11156 sketch_id,
11157 constraint_id,
11158 Constraint::HorizontalDistance(Distance {
11159 segments: vec![point0_id.into(), point1_id.into()],
11160 distance: Number {
11161 value: 4.0,
11162 units: NumericSuffix::Mm,
11163 },
11164 label_position: Some(label_position.clone()),
11165 source: Default::default(),
11166 }),
11167 EditConstraintOptions {
11168 commit_solved_initial_guesses: false,
11169 },
11170 )
11171 .await
11172 .unwrap();
11173 assert_eq!(
11174 source_delta.text,
11175 "\
11176sketch(on = XY) {
11177 line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 3mm])
11178 horizontalDistance([line1.start, line1.end], labelPosition = [2mm, 5mm]) == 4mm
11179}
11180"
11181 );
11182
11183 let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
11184 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11185 panic!("Expected constraint object");
11186 };
11187 let Constraint::HorizontalDistance(distance) = constraint else {
11188 panic!("Expected horizontal distance constraint");
11189 };
11190 assert_eq!(distance.distance.value, 4.0);
11191 assert_eq!(distance.label_position, Some(label_position));
11192
11193 mock_ctx.close().await;
11194 }
11195
11196 #[tokio::test(flavor = "multi_thread")]
11197 async fn test_edit_angle_constraint_label_position() {
11198 let initial_source = "\
11199sketch(on = XY) {
11200 line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11201 line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
11202 angle([line1, line2]) == 60deg
11203}
11204";
11205
11206 let program = Program::parse(initial_source).unwrap().0.unwrap();
11207 let mut frontend = FrontendState::new();
11208 let mock_ctx = ExecutorContext::new_mock(None).await;
11209 let version = Version(0);
11210
11211 frontend.program = program.clone();
11212 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11213 frontend.update_state_after_exec(outcome, true);
11214 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11215 let sketch_id = sketch_object.id;
11216 let sketch = expect_sketch(sketch_object);
11217 let constraint_id = sketch.constraints[0];
11218 let label_position = Point2d {
11219 x: Number {
11220 value: 10.0,
11221 units: NumericSuffix::Mm,
11222 },
11223 y: Number {
11224 value: 11.0,
11225 units: NumericSuffix::Mm,
11226 },
11227 };
11228
11229 let (src_delta, scene_delta) = frontend
11230 .edit_distance_constraint_label_position(
11231 &mock_ctx,
11232 version,
11233 sketch_id,
11234 constraint_id,
11235 label_position.clone(),
11236 vec![],
11237 )
11238 .await
11239 .unwrap();
11240 assert_eq!(
11241 src_delta.text.as_str(),
11242 "\
11243sketch(on = XY) {
11244 line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11245 line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.46mm])
11246 angle([line1, line2], labelPosition = [10mm, 11mm]) == 60deg
11247}
11248"
11249 );
11250
11251 let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
11252 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11253 panic!("Expected constraint object");
11254 };
11255 let Constraint::Angle(angle) = constraint else {
11256 panic!("Expected angle constraint");
11257 };
11258 assert_eq!(angle.label_position, Some(label_position));
11259
11260 mock_ctx.close().await;
11261 }
11262
11263 #[tokio::test(flavor = "multi_thread")]
11264 async fn test_edit_angle_constraint_label_position_with_call_on_right() {
11265 let initial_source = "\
11266sketch(on = XY) {
11267 line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11268 line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
11269 60deg == angleDimension(lines = [line1, line2], sector = 1)
11270}
11271";
11272
11273 let program = Program::parse(initial_source).unwrap().0.unwrap();
11274 let mut frontend = FrontendState::new();
11275 let mock_ctx = ExecutorContext::new_mock(None).await;
11276 let version = Version(0);
11277
11278 frontend.program = program.clone();
11279 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11280 frontend.update_state_after_exec(outcome, true);
11281 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11282 let sketch_id = sketch_object.id;
11283 let sketch = expect_sketch(sketch_object);
11284 let constraint_id = sketch.constraints[0];
11285 let label_position = Point2d {
11286 x: Number {
11287 value: 10.0,
11288 units: NumericSuffix::Mm,
11289 },
11290 y: Number {
11291 value: 11.0,
11292 units: NumericSuffix::Mm,
11293 },
11294 };
11295
11296 let (src_delta, scene_delta) = frontend
11297 .edit_distance_constraint_label_position(
11298 &mock_ctx,
11299 version,
11300 sketch_id,
11301 constraint_id,
11302 label_position.clone(),
11303 vec![],
11304 )
11305 .await
11306 .unwrap();
11307 assert_eq!(
11308 src_delta.text.as_str(),
11309 "\
11310sketch(on = XY) {
11311 line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11312 line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.46mm])
11313 60deg == angleDimension(lines = [line1, line2], sector = 1, labelPosition = [10mm, 11mm])
11314}
11315"
11316 );
11317
11318 let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
11319 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11320 panic!("Expected constraint object");
11321 };
11322 let Constraint::Angle(angle) = constraint else {
11323 panic!("Expected angle constraint");
11324 };
11325 assert_eq!(angle.label_position, Some(label_position));
11326
11327 mock_ctx.close().await;
11328 }
11329
11330 #[tokio::test(flavor = "multi_thread")]
11331 async fn test_edit_angle_constraint() {
11332 let initial_source = "\
11333sketch(on = XY) {
11334 line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11335 line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
11336 angle([line1, line2]) == 60deg
11337}
11338";
11339
11340 let program = Program::parse(initial_source).unwrap().0.unwrap();
11341 let mut frontend = FrontendState::new();
11342 let mock_ctx = ExecutorContext::new_mock(None).await;
11343 let version = Version(0);
11344
11345 frontend.program = program.clone();
11346 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11347 frontend.update_state_after_exec(outcome, true);
11348 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11349 let sketch_id = sketch_object.id;
11350 let sketch = expect_sketch(sketch_object);
11351 let constraint_id = sketch.constraints[0];
11352 let line1_id = *sketch.segments.get(2).unwrap();
11353 let line2_id = *sketch.segments.get(5).unwrap();
11354 let label_position = Point2d {
11355 x: Number {
11356 value: 10.0,
11357 units: NumericSuffix::Mm,
11358 },
11359 y: Number {
11360 value: 11.0,
11361 units: NumericSuffix::Mm,
11362 },
11363 };
11364
11365 let (src_delta, scene_delta) = frontend
11366 .edit_angle_constraint_with_options(
11367 &mock_ctx,
11368 version,
11369 sketch_id,
11370 constraint_id,
11371 Angle {
11372 lines: vec![line2_id, line1_id],
11373 angle: Number {
11374 value: 60.0,
11375 units: NumericSuffix::Deg,
11376 },
11377 sector: Some(3),
11378 inverse: Some(false),
11379 label_position: Some(label_position.clone()),
11380 source: Default::default(),
11381 },
11382 EditConstraintOptions {
11383 commit_solved_initial_guesses: false,
11384 },
11385 )
11386 .await
11387 .unwrap();
11388 assert_eq!(
11389 src_delta.text.as_str(),
11390 "\
11391sketch(on = XY) {
11392 line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11393 line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
11394 angleDimension(lines = [line2, line1], sector = 3, labelPosition = [10mm, 11mm]) == 60deg
11395}
11396"
11397 );
11398
11399 let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
11400 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11401 panic!("Expected constraint object");
11402 };
11403 let Constraint::Angle(angle) = constraint else {
11404 panic!("Expected angle constraint");
11405 };
11406 assert_eq!(angle.lines, vec![line2_id, line1_id]);
11407 assert_eq!(angle.sector, Some(3));
11408 assert_eq!(angle.inverse, Some(false));
11409 assert_eq!(angle.label_position, Some(label_position));
11410
11411 mock_ctx.close().await;
11412 }
11413
11414 #[tokio::test(flavor = "multi_thread")]
11415 async fn test_edit_angle_constraint_with_call_on_right() {
11416 let initial_source = "\
11417sketch(on = XY) {
11418 line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11419 line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
11420 60deg == angle([line1, line2])
11421}
11422";
11423
11424 let program = Program::parse(initial_source).unwrap().0.unwrap();
11425 let mut frontend = FrontendState::new();
11426 let mock_ctx = ExecutorContext::new_mock(None).await;
11427 let version = Version(0);
11428
11429 frontend.program = program.clone();
11430 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11431 frontend.update_state_after_exec(outcome, true);
11432 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11433 let sketch_id = sketch_object.id;
11434 let sketch = expect_sketch(sketch_object);
11435 let constraint_id = sketch.constraints[0];
11436 let line1_id = *sketch.segments.get(2).unwrap();
11437 let line2_id = *sketch.segments.get(5).unwrap();
11438
11439 let (src_delta, _) = frontend
11440 .edit_angle_constraint_with_options(
11441 &mock_ctx,
11442 version,
11443 sketch_id,
11444 constraint_id,
11445 Angle {
11446 lines: vec![line2_id, line1_id],
11447 angle: Number {
11448 value: 60.0,
11449 units: NumericSuffix::Deg,
11450 },
11451 sector: Some(3),
11452 inverse: Some(false),
11453 label_position: None,
11454 source: Default::default(),
11455 },
11456 EditConstraintOptions {
11457 commit_solved_initial_guesses: false,
11458 },
11459 )
11460 .await
11461 .unwrap();
11462 assert_eq!(
11463 src_delta.text.as_str(),
11464 "\
11465sketch(on = XY) {
11466 line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11467 line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
11468 60deg == angleDimension(lines = [line2, line1], sector = 3)
11469}
11470"
11471 );
11472
11473 mock_ctx.close().await;
11474 }
11475
11476 #[tokio::test(flavor = "multi_thread")]
11477 async fn test_edit_segments_can_commit_constraint_label_position_in_same_execution() {
11478 let initial_source = "\
11479@settings(kclVersion = 2.0)
11480
11481sketch001 = sketch(on = XZ) {
11482 line1 = line(start = [var 0mm, var 12.55mm], end = [var -6.03mm, var 8.51mm])
11483 line3 = line(start = [var -7.41mm, var 2.92mm], end = [var -1.47mm, var 4.32mm])
11484 distance([line1.start, line3.end], labelPosition = [5.56mm, 8.65mm]) == 8.36mm
11485 vertical([line1.start, ORIGIN])
11486}
11487";
11488
11489 let program = Program::parse(initial_source).unwrap().0.unwrap();
11490 let mut frontend = FrontendState::new();
11491 let mock_ctx = ExecutorContext::new_mock(None).await;
11492 let version = Version(0);
11493
11494 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
11495 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11496 let sketch_id = sketch_object.id;
11497 let sketch = expect_sketch(sketch_object);
11498 let constraint_id = sketch
11499 .constraints
11500 .iter()
11501 .copied()
11502 .find(|constraint_id| {
11503 matches!(
11504 frontend.scene_graph.objects[constraint_id.0].kind,
11505 ObjectKind::Constraint {
11506 constraint: Constraint::Distance(_)
11507 }
11508 )
11509 })
11510 .unwrap();
11511 let line1_id = sketch
11512 .segments
11513 .iter()
11514 .copied()
11515 .find(|segment_id| {
11516 matches!(
11517 frontend.scene_graph.objects[segment_id.0].kind,
11518 ObjectKind::Segment {
11519 segment: Segment::Line(_)
11520 }
11521 )
11522 })
11523 .unwrap();
11524 let label_position = Point2d {
11525 x: Number {
11526 value: 7.0,
11527 units: NumericSuffix::Mm,
11528 },
11529 y: Number {
11530 value: 9.0,
11531 units: NumericSuffix::Mm,
11532 },
11533 };
11534
11535 let (source_delta, scene_delta) = frontend
11536 .edit_segments_with_options(
11537 &mock_ctx,
11538 version,
11539 sketch_id,
11540 vec![ExistingSegmentCtor {
11541 id: line1_id,
11542 ctor: SegmentCtor::Line(LineCtor {
11543 start: point_expr_mm(2.0, 15.55),
11544 end: point_expr_mm(-4.03, 11.51),
11545 construction: None,
11546 }),
11547 }],
11548 EditSegmentsOptions {
11549 anchor_segment_ids: Some(vec![]),
11550 drag_anchors: vec![SegmentDragAnchor {
11551 segment_id: line1_id,
11552 target: label_position.clone(),
11553 }],
11554 constraint_label_edits: vec![ConstraintLabelPositionEdit {
11555 constraint_id,
11556 label_position: label_position.clone(),
11557 }],
11558 commit_solved_initial_guesses: true,
11559 },
11560 )
11561 .await
11562 .unwrap();
11563
11564 assert!(source_delta.text.contains("labelPosition = [7mm, 9mm]"));
11565 let constraint_object = &scene_delta.new_graph.objects[constraint_id.0];
11566 let ObjectKind::Constraint {
11567 constraint: Constraint::Distance(distance),
11568 } = &constraint_object.kind
11569 else {
11570 panic!("Expected distance constraint object");
11571 };
11572 assert_eq!(distance.label_position, Some(label_position));
11573
11574 let snapped_label_position = Point2d {
11575 x: Number {
11576 value: 8.0,
11577 units: NumericSuffix::Mm,
11578 },
11579 y: Number {
11580 value: 10.0,
11581 units: NumericSuffix::Mm,
11582 },
11583 };
11584 let (source_delta, scene_delta) = frontend
11585 .edit_segments_with_options(
11586 &mock_ctx,
11587 version,
11588 sketch_id,
11589 vec![],
11590 EditSegmentsOptions {
11591 anchor_segment_ids: Some(vec![line1_id]),
11592 drag_anchors: vec![],
11593 constraint_label_edits: vec![ConstraintLabelPositionEdit {
11594 constraint_id,
11595 label_position: snapped_label_position.clone(),
11596 }],
11597 commit_solved_initial_guesses: true,
11598 },
11599 )
11600 .await
11601 .unwrap();
11602
11603 assert!(source_delta.text.contains("labelPosition = [8mm, 10mm]"));
11604 let constraint_object = &scene_delta.new_graph.objects[constraint_id.0];
11605 let ObjectKind::Constraint {
11606 constraint: Constraint::Distance(distance),
11607 } = &constraint_object.kind
11608 else {
11609 panic!("Expected distance constraint object");
11610 };
11611 assert_eq!(distance.label_position, Some(snapped_label_position));
11612
11613 mock_ctx.close().await;
11614 }
11615
11616 #[tokio::test(flavor = "multi_thread")]
11617 async fn test_edit_distance_constraint_label_position_preserves_anchor_segment_solution() {
11618 let initial_source = "\
11619sketch(on = XY) {
11620 point1 = point(at = [var 0mm, var 0mm])
11621 point2 = point(at = [var 10mm, var 0mm])
11622 distance([point1, point2]) == 5mm
11623}
11624";
11625
11626 let program = Program::parse(initial_source).unwrap().0.unwrap();
11627 let mut frontend = FrontendState::new();
11628 let mock_ctx = ExecutorContext::new_mock(None).await;
11629 let version = Version(0);
11630
11631 frontend.program = program.clone();
11632 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11633 frontend.update_state_after_exec(outcome, true);
11634 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11635 let sketch_id = sketch_object.id;
11636 let sketch = expect_sketch(sketch_object);
11637 let point0_id = sketch.segments[0];
11638 let point1_id = sketch.segments[1];
11639 let constraint_id = sketch.constraints[0];
11640
11641 let edited_segments = vec![ExistingSegmentCtor {
11642 id: point0_id,
11643 ctor: SegmentCtor::Point(PointCtor {
11644 position: Point2d {
11645 x: Expr::Var(Number {
11646 value: 2.0,
11647 units: NumericSuffix::Mm,
11648 }),
11649 y: Expr::Var(Number {
11650 value: 1.0,
11651 units: NumericSuffix::Mm,
11652 }),
11653 },
11654 }),
11655 }];
11656 let (_, scene_delta) = frontend
11657 .edit_segments(&mock_ctx, version, sketch_id, edited_segments)
11658 .await
11659 .unwrap();
11660 let point0_after_segment_edit = point_position(&scene_delta.new_graph, point0_id);
11661 let point1_after_segment_edit = point_position(&scene_delta.new_graph, point1_id);
11662
11663 let label_position = Point2d {
11664 x: Number {
11665 value: 3.0,
11666 units: NumericSuffix::Mm,
11667 },
11668 y: Number {
11669 value: 4.0,
11670 units: NumericSuffix::Mm,
11671 },
11672 };
11673 let (_, scene_delta) = frontend
11674 .edit_distance_constraint_label_position(
11675 &mock_ctx,
11676 version,
11677 sketch_id,
11678 constraint_id,
11679 label_position,
11680 vec![point0_id],
11681 )
11682 .await
11683 .unwrap();
11684
11685 assert_point_position_close(
11686 point_position(&scene_delta.new_graph, point0_id),
11687 point0_after_segment_edit,
11688 );
11689 assert_point_position_close(
11690 point_position(&scene_delta.new_graph, point1_id),
11691 point1_after_segment_edit,
11692 );
11693
11694 mock_ctx.close().await;
11695 }
11696
11697 #[tokio::test(flavor = "multi_thread")]
11698 async fn test_distance_point_line() {
11699 let initial_source = "\
11700sketch(on = XY) {
11701 point(at = [var 0, var 5])
11702 line(start = [var 0, var 0], end = [var 10, var 0])
11703}
11704";
11705
11706 let program = Program::parse(initial_source).unwrap().0.unwrap();
11707
11708 let mut frontend = FrontendState::new();
11709
11710 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
11711 let mock_ctx = ExecutorContext::new_mock(None).await;
11712 let version = Version(0);
11713
11714 let outcome = frontend.hack_set_program(&ctx, program).await.unwrap();
11715 assert!(matches!(outcome, SetProgramOutcome::Success { .. }), "{outcome:?}");
11716 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11717 let sketch_id = sketch_object.id;
11718 let sketch = expect_sketch(sketch_object);
11719 let point_id = *sketch.segments.first().unwrap();
11720 let line_id = *sketch
11721 .segments
11722 .iter()
11723 .find(|segment_id| {
11724 matches!(
11725 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11726 Some(ObjectKind::Segment {
11727 segment: Segment::Line(_)
11728 })
11729 )
11730 })
11731 .unwrap();
11732
11733 let label_position = Point2d {
11734 x: Number {
11735 value: 10.0,
11736 units: NumericSuffix::Mm,
11737 },
11738 y: Number {
11739 value: 11.0,
11740 units: NumericSuffix::Mm,
11741 },
11742 };
11743 let constraint = Constraint::Distance(Distance {
11744 segments: vec![point_id.into(), line_id.into()],
11745 distance: Number {
11746 value: 5.0,
11747 units: NumericSuffix::Mm,
11748 },
11749 label_position: Some(label_position.clone()),
11750 source: Default::default(),
11751 });
11752 let (src_delta, scene_delta) = frontend
11753 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11754 .await
11755 .unwrap();
11756 insta::assert_snapshot!("test_distance_point_line", src_delta.text.as_str());
11757 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
11758 let sketch = expect_sketch(sketch_object);
11759 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
11760 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11761 panic!("Expected constraint object");
11762 };
11763 let Constraint::Distance(distance) = constraint else {
11764 panic!("Expected distance constraint");
11765 };
11766 assert_eq!(distance.label_position, Some(label_position));
11767
11768 ctx.close().await;
11769 mock_ctx.close().await;
11770 }
11771
11772 #[tokio::test(flavor = "multi_thread")]
11773 async fn test_distance_point_arc() {
11774 let initial_source = "\
11775sketch(on = XY) {
11776 point(at = [var 0, var 8])
11777 arc(start = [var 5, var 0], end = [var 0, var 5], center = [var 0, var 0])
11778}
11779";
11780
11781 let program = Program::parse(initial_source).unwrap().0.unwrap();
11782
11783 let mut frontend = FrontendState::new();
11784
11785 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
11786 let mock_ctx = ExecutorContext::new_mock(None).await;
11787 let version = Version(0);
11788
11789 frontend.hack_set_program(&ctx, program).await.unwrap();
11790 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11791 let sketch_id = sketch_object.id;
11792 let sketch = expect_sketch(sketch_object);
11793 let point_id = *sketch.segments.first().unwrap();
11794 let arc_id = *sketch
11795 .segments
11796 .iter()
11797 .find(|segment_id| {
11798 matches!(
11799 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11800 Some(ObjectKind::Segment {
11801 segment: Segment::Arc(_)
11802 })
11803 )
11804 })
11805 .unwrap();
11806
11807 let constraint = Constraint::Distance(Distance {
11808 segments: vec![point_id.into(), arc_id.into()],
11809 distance: Number {
11810 value: 3.0,
11811 units: NumericSuffix::Mm,
11812 },
11813 label_position: None,
11814 source: Default::default(),
11815 });
11816 let (src_delta, _scene_delta) = frontend
11817 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11818 .await
11819 .unwrap();
11820 insta::assert_snapshot!("test_distance_point_arc", src_delta.text.as_str());
11821
11822 ctx.close().await;
11823 mock_ctx.close().await;
11824 }
11825
11826 #[tokio::test(flavor = "multi_thread")]
11827 async fn test_distance_arc_origin() {
11828 let initial_source = "\
11829sketch001 = sketch(on = XY) {
11830 arc(start = [var -4.13mm, var -0.59mm], end = [var -3.47mm, var 3.38mm], center = [var -4.55mm, var 1.52mm])
11831}
11832";
11833
11834 let program = Program::parse(initial_source).unwrap().0.unwrap();
11835
11836 let mut frontend = FrontendState::new();
11837
11838 let mock_ctx = ExecutorContext::new_mock(None).await;
11839 let version = Version(0);
11840
11841 frontend.program = program.clone();
11842 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11843 frontend.update_state_after_exec(outcome, true);
11844 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11845 let sketch_id = sketch_object.id;
11846 let sketch = expect_sketch(sketch_object);
11847 let arc_id = *sketch
11848 .segments
11849 .iter()
11850 .find(|segment_id| {
11851 matches!(
11852 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11853 Some(ObjectKind::Segment {
11854 segment: Segment::Arc(_)
11855 })
11856 )
11857 })
11858 .unwrap();
11859
11860 let constraint = Constraint::Distance(Distance {
11861 segments: vec![arc_id.into(), ConstraintSegment::ORIGIN],
11862 distance: Number {
11863 value: 3.0,
11864 units: NumericSuffix::Mm,
11865 },
11866 label_position: None,
11867 source: Default::default(),
11868 });
11869 let (src_delta, _scene_delta) = frontend
11870 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11871 .await
11872 .unwrap();
11873 insta::assert_snapshot!("test_distance_arc_origin", src_delta.text.as_str());
11874
11875 mock_ctx.close().await;
11876 }
11877
11878 #[tokio::test(flavor = "multi_thread")]
11879 async fn test_distance_line_origin() {
11880 let initial_source = "\
11881sketch(on = XY) {
11882 line(start = [var 5, var 0], end = [var 5, var 10])
11883}
11884";
11885
11886 let program = Program::parse(initial_source).unwrap().0.unwrap();
11887
11888 let mut frontend = FrontendState::new();
11889
11890 let mock_ctx = ExecutorContext::new_mock(None).await;
11891 let version = Version(0);
11892
11893 frontend.program = program.clone();
11894 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11895 frontend.update_state_after_exec(outcome, true);
11896 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11897 let sketch_id = sketch_object.id;
11898 let sketch = expect_sketch(sketch_object);
11899 let line_id = *sketch
11900 .segments
11901 .iter()
11902 .find(|segment_id| {
11903 matches!(
11904 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11905 Some(ObjectKind::Segment {
11906 segment: Segment::Line(_)
11907 })
11908 )
11909 })
11910 .unwrap();
11911
11912 let constraint = Constraint::Distance(Distance {
11913 segments: vec![ConstraintSegment::ORIGIN, line_id.into()],
11914 distance: Number {
11915 value: 5.0,
11916 units: NumericSuffix::Mm,
11917 },
11918 label_position: None,
11919 source: Default::default(),
11920 });
11921 let (src_delta, _scene_delta) = frontend
11922 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11923 .await
11924 .unwrap();
11925 insta::assert_snapshot!("test_distance_line_origin", src_delta.text.as_str());
11926
11927 mock_ctx.close().await;
11928 }
11929
11930 #[tokio::test(flavor = "multi_thread")]
11931 async fn test_distance_line_circle() {
11932 let initial_source = "\
11933sketch(on = XY) {
11934 line(start = [var -10, var 8], end = [var 10, var 8])
11935 circle(start = [var 5, var 0], center = [var 0, var 0])
11936}
11937";
11938
11939 let program = Program::parse(initial_source).unwrap().0.unwrap();
11940
11941 let mut frontend = FrontendState::new();
11942
11943 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
11944 let mock_ctx = ExecutorContext::new_mock(None).await;
11945 let version = Version(0);
11946
11947 frontend.hack_set_program(&ctx, program).await.unwrap();
11948 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11949 let sketch_id = sketch_object.id;
11950 let sketch = expect_sketch(sketch_object);
11951 let line_id = *sketch
11952 .segments
11953 .iter()
11954 .find(|segment_id| {
11955 matches!(
11956 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11957 Some(ObjectKind::Segment {
11958 segment: Segment::Line(_)
11959 })
11960 )
11961 })
11962 .unwrap();
11963 let circle_id = *sketch
11964 .segments
11965 .iter()
11966 .find(|segment_id| {
11967 matches!(
11968 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11969 Some(ObjectKind::Segment {
11970 segment: Segment::Circle(_)
11971 })
11972 )
11973 })
11974 .unwrap();
11975
11976 let constraint = Constraint::Distance(Distance {
11977 segments: vec![line_id.into(), circle_id.into()],
11978 distance: Number {
11979 value: 3.0,
11980 units: NumericSuffix::Mm,
11981 },
11982 label_position: None,
11983 source: Default::default(),
11984 });
11985 let (src_delta, _scene_delta) = frontend
11986 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11987 .await
11988 .unwrap();
11989 insta::assert_snapshot!("test_distance_line_circle", src_delta.text.as_str());
11990
11991 ctx.close().await;
11992 mock_ctx.close().await;
11993 }
11994
11995 #[tokio::test(flavor = "multi_thread")]
11996 async fn test_distance_circle_arc() {
11997 let initial_source = "\
11998sketch(on = XY) {
11999 circle(start = [var 5, var 0], center = [var 0, var 0])
12000 arc(start = [var 15, var 0], end = [var 10, var 5], center = [var 10, var 0])
12001}
12002";
12003
12004 let program = Program::parse(initial_source).unwrap().0.unwrap();
12005
12006 let mut frontend = FrontendState::new();
12007
12008 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
12009 let mock_ctx = ExecutorContext::new_mock(None).await;
12010 let version = Version(0);
12011
12012 let outcome = frontend.hack_set_program(&ctx, program).await.unwrap();
12013 assert!(matches!(outcome, SetProgramOutcome::Success { .. }), "{outcome:?}");
12014 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12015 let sketch_id = sketch_object.id;
12016 let sketch = expect_sketch(sketch_object);
12017 let circle_id = *sketch
12018 .segments
12019 .iter()
12020 .find(|segment_id| {
12021 matches!(
12022 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
12023 Some(ObjectKind::Segment {
12024 segment: Segment::Circle(_)
12025 })
12026 )
12027 })
12028 .unwrap();
12029 let arc_id = *sketch
12030 .segments
12031 .iter()
12032 .find(|segment_id| {
12033 matches!(
12034 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
12035 Some(ObjectKind::Segment {
12036 segment: Segment::Arc(_)
12037 })
12038 )
12039 })
12040 .unwrap();
12041
12042 let constraint = Constraint::Distance(Distance {
12043 segments: vec![circle_id.into(), arc_id.into()],
12044 distance: Number {
12045 value: 3.0,
12046 units: NumericSuffix::Mm,
12047 },
12048 label_position: None,
12049 source: Default::default(),
12050 });
12051 let (src_delta, _scene_delta) = frontend
12052 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12053 .await
12054 .unwrap();
12055 insta::assert_snapshot!("test_distance_circle_arc", src_delta.text.as_str());
12056
12057 ctx.close().await;
12058 mock_ctx.close().await;
12059 }
12060
12061 #[tokio::test(flavor = "multi_thread")]
12062 async fn test_distance_parallel_lines() {
12063 let initial_source = "\
12064sketch(on = XY) {
12065 line(start = [var 0, var 0], end = [var 10, var 0])
12066 line(start = [var 0, var 5], end = [var 10, var 5])
12067}
12068";
12069
12070 let program = Program::parse(initial_source).unwrap().0.unwrap();
12071
12072 let mut frontend = FrontendState::new();
12073
12074 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
12075 let mock_ctx = ExecutorContext::new_mock(None).await;
12076 let version = Version(0);
12077
12078 frontend.hack_set_program(&ctx, program).await.unwrap();
12079 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12080 let sketch_id = sketch_object.id;
12081 let sketch = expect_sketch(sketch_object);
12082 let line_ids = sketch
12083 .segments
12084 .iter()
12085 .copied()
12086 .filter(|segment_id| {
12087 matches!(
12088 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
12089 Some(ObjectKind::Segment {
12090 segment: Segment::Line(_)
12091 })
12092 )
12093 })
12094 .collect::<Vec<_>>();
12095
12096 let constraint = Constraint::Distance(Distance {
12097 segments: vec![line_ids[0].into(), line_ids[1].into()],
12098 distance: Number {
12099 value: 5.0,
12100 units: NumericSuffix::Mm,
12101 },
12102 label_position: None,
12103 source: Default::default(),
12104 });
12105 let (src_delta, _scene_delta) = frontend
12106 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12107 .await
12108 .unwrap();
12109 insta::assert_snapshot!("test_distance_parallel_lines", src_delta.text.as_str());
12110
12111 ctx.close().await;
12112 mock_ctx.close().await;
12113 }
12114
12115 #[tokio::test(flavor = "multi_thread")]
12116 async fn test_distance_non_parallel_lines_lowers_to_distance() {
12117 let initial_source = "\
12122sketch(on = XY) {
12123 line(start = [var 0, var 0], end = [var 10, var 0])
12124 line(start = [var 0, var 0], end = [var 10, var 10])
12125}
12126";
12127
12128 let program = Program::parse(initial_source).unwrap().0.unwrap();
12129
12130 let mut frontend = FrontendState::new();
12131
12132 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
12133 let mock_ctx = ExecutorContext::new_mock(None).await;
12134 let version = Version(0);
12135
12136 frontend.hack_set_program(&ctx, program).await.unwrap();
12137 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12138 let sketch_id = sketch_object.id;
12139 let sketch = expect_sketch(sketch_object);
12140 let line_ids = sketch
12141 .segments
12142 .iter()
12143 .copied()
12144 .filter(|segment_id| {
12145 matches!(
12146 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
12147 Some(ObjectKind::Segment {
12148 segment: Segment::Line(_)
12149 })
12150 )
12151 })
12152 .collect::<Vec<_>>();
12153
12154 let constraint = Constraint::Distance(Distance {
12155 segments: vec![line_ids[0].into(), line_ids[1].into()],
12156 distance: Number {
12157 value: 5.0,
12158 units: NumericSuffix::Mm,
12159 },
12160 label_position: None,
12161 source: Default::default(),
12162 });
12163 let (src_delta, _scene_delta) = frontend
12164 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12165 .await
12166 .unwrap();
12167 insta::assert_snapshot!(
12168 "test_distance_non_parallel_lines_lowers_to_distance",
12169 src_delta.text.as_str()
12170 );
12171
12172 ctx.close().await;
12173 mock_ctx.close().await;
12174 }
12175
12176 #[tokio::test(flavor = "multi_thread")]
12177 async fn test_horizontal_distance_two_points() {
12178 let initial_source = "\
12179sketch(on = XY) {
12180 point(at = [var 1, var 2])
12181 point(at = [var 3, var 4])
12182}
12183";
12184
12185 let program = Program::parse(initial_source).unwrap().0.unwrap();
12186
12187 let mut frontend = FrontendState::new();
12188
12189 let mock_ctx = ExecutorContext::new_mock(None).await;
12190 let version = Version(0);
12191
12192 frontend.program = program.clone();
12193 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12194 frontend.update_state_after_exec(outcome, true);
12195 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12196 let sketch_id = sketch_object.id;
12197 let sketch = expect_sketch(sketch_object);
12198 let point0_id = *sketch.segments.first().unwrap();
12199 let point1_id = *sketch.segments.get(1).unwrap();
12200 let label_position = Point2d {
12201 x: Number {
12202 value: 10.0,
12203 units: NumericSuffix::Mm,
12204 },
12205 y: Number {
12206 value: 11.0,
12207 units: NumericSuffix::Mm,
12208 },
12209 };
12210
12211 let constraint = Constraint::HorizontalDistance(Distance {
12212 segments: vec![point0_id.into(), point1_id.into()],
12213 distance: Number {
12214 value: 2.0,
12215 units: NumericSuffix::Mm,
12216 },
12217 label_position: Some(label_position.clone()),
12218 source: Default::default(),
12219 });
12220 let (src_delta, scene_delta) = frontend
12221 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12222 .await
12223 .unwrap();
12224 insta::assert_snapshot!("test_horizontal_distance_two_points", src_delta.text.as_str());
12225 assert_eq!(
12226 scene_delta.new_graph.objects.len(),
12227 5,
12228 "{:#?}",
12229 scene_delta.new_graph.objects
12230 );
12231 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
12232 let sketch = expect_sketch(sketch_object);
12233 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
12234 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12235 panic!("Expected constraint object");
12236 };
12237 let Constraint::HorizontalDistance(distance) = constraint else {
12238 panic!("Expected horizontal distance constraint");
12239 };
12240 assert_eq!(distance.label_position, Some(label_position));
12241
12242 mock_ctx.close().await;
12243 }
12244
12245 #[tokio::test(flavor = "multi_thread")]
12246 async fn test_radius_single_arc_segment() {
12247 let initial_source = "\
12248sketch(on = XY) {
12249 arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
12250}
12251";
12252
12253 let program = Program::parse(initial_source).unwrap().0.unwrap();
12254
12255 let mut frontend = FrontendState::new();
12256
12257 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
12258 let mock_ctx = ExecutorContext::new_mock(None).await;
12259 let version = Version(0);
12260
12261 frontend.hack_set_program(&ctx, program).await.unwrap();
12262 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12263 let sketch_id = sketch_object.id;
12264 let sketch = expect_sketch(sketch_object);
12265 let arc_id = sketch
12267 .segments
12268 .iter()
12269 .find(|&seg_id| {
12270 let obj = frontend.scene_graph.objects.get(seg_id.0);
12271 matches!(
12272 obj.map(|o| &o.kind),
12273 Some(ObjectKind::Segment {
12274 segment: Segment::Arc(_)
12275 })
12276 )
12277 })
12278 .unwrap();
12279
12280 let constraint = Constraint::Radius(Radius {
12281 arc: *arc_id,
12282 radius: Number {
12283 value: 5.0,
12284 units: NumericSuffix::Mm,
12285 },
12286 label_position: None,
12287 source: Default::default(),
12288 });
12289 let (src_delta, scene_delta) = frontend
12290 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12291 .await
12292 .unwrap();
12293 insta::assert_snapshot!("test_radius_single_arc_segment", src_delta.text.as_str());
12294 assert_eq!(
12295 scene_delta.new_graph.objects.len(),
12296 7, "{:#?}",
12298 scene_delta.new_graph.objects
12299 );
12300
12301 ctx.close().await;
12302 mock_ctx.close().await;
12303 }
12304
12305 #[tokio::test(flavor = "multi_thread")]
12306 async fn test_radius_single_arc_segment_with_label_position() {
12307 let initial_source = "\
12308sketch(on = XY) {
12309 arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
12310}
12311";
12312
12313 let program = Program::parse(initial_source).unwrap().0.unwrap();
12314 let mut frontend = FrontendState::new();
12315 let mock_ctx = ExecutorContext::new_mock(None).await;
12316 let version = Version(0);
12317
12318 frontend.program = program.clone();
12319 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12320 frontend.update_state_after_exec(outcome, true);
12321 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12322 let sketch_id = sketch_object.id;
12323 let sketch = expect_sketch(sketch_object);
12324 let arc_id = sketch
12325 .segments
12326 .iter()
12327 .find(|&seg_id| {
12328 let obj = frontend.scene_graph.objects.get(seg_id.0);
12329 matches!(
12330 obj.map(|o| &o.kind),
12331 Some(ObjectKind::Segment {
12332 segment: Segment::Arc(_)
12333 })
12334 )
12335 })
12336 .unwrap();
12337
12338 let label_position = Point2d {
12339 x: Number {
12340 value: 10.0,
12341 units: NumericSuffix::Mm,
12342 },
12343 y: Number {
12344 value: 11.0,
12345 units: NumericSuffix::Mm,
12346 },
12347 };
12348 let constraint = Constraint::Radius(Radius {
12349 arc: *arc_id,
12350 radius: Number {
12351 value: 5.0,
12352 units: NumericSuffix::Mm,
12353 },
12354 label_position: Some(label_position.clone()),
12355 source: Default::default(),
12356 });
12357 let (src_delta, scene_delta) = frontend
12358 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12359 .await
12360 .unwrap();
12361 insta::assert_snapshot!(
12362 "test_radius_single_arc_segment_with_label_position",
12363 src_delta.text.as_str()
12364 );
12365
12366 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
12367 let sketch = expect_sketch(sketch_object);
12368 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
12369 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12370 panic!("Expected constraint object");
12371 };
12372 let Constraint::Radius(radius) = constraint else {
12373 panic!("Expected radius constraint");
12374 };
12375 assert_eq!(radius.label_position, Some(label_position));
12376
12377 mock_ctx.close().await;
12378 }
12379
12380 #[tokio::test(flavor = "multi_thread")]
12381 async fn test_edit_radius_constraint_label_position() {
12382 let initial_source = "\
12383sketch(on = XY) {
12384 arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
12385 radius(arc1) == 5mm
12386}
12387";
12388
12389 let program = Program::parse(initial_source).unwrap().0.unwrap();
12390 let mut frontend = FrontendState::new();
12391 let mock_ctx = ExecutorContext::new_mock(None).await;
12392 let version = Version(0);
12393
12394 frontend.program = program.clone();
12395 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12396 frontend.update_state_after_exec(outcome, true);
12397 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12398 let sketch_id = sketch_object.id;
12399 let sketch = expect_sketch(sketch_object);
12400 let constraint_id = sketch.constraints[0];
12401 let label_position = Point2d {
12402 x: Number {
12403 value: 10.0,
12404 units: NumericSuffix::Mm,
12405 },
12406 y: Number {
12407 value: 11.0,
12408 units: NumericSuffix::Mm,
12409 },
12410 };
12411
12412 let (src_delta, scene_delta) = frontend
12413 .edit_distance_constraint_label_position(
12414 &mock_ctx,
12415 version,
12416 sketch_id,
12417 constraint_id,
12418 label_position.clone(),
12419 vec![],
12420 )
12421 .await
12422 .unwrap();
12423 insta::assert_snapshot!("test_edit_radius_constraint_label_position", src_delta.text.as_str());
12424
12425 let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
12426 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12427 panic!("Expected constraint object");
12428 };
12429 let Constraint::Radius(radius) = constraint else {
12430 panic!("Expected radius constraint");
12431 };
12432 assert_eq!(radius.label_position, Some(label_position));
12433
12434 mock_ctx.close().await;
12435 }
12436
12437 #[tokio::test(flavor = "multi_thread")]
12438 async fn test_vertical_distance_two_points() {
12439 let initial_source = "\
12440sketch(on = XY) {
12441 point(at = [var 1, var 2])
12442 point(at = [var 3, var 4])
12443}
12444";
12445
12446 let program = Program::parse(initial_source).unwrap().0.unwrap();
12447
12448 let mut frontend = FrontendState::new();
12449
12450 let mock_ctx = ExecutorContext::new_mock(None).await;
12451 let version = Version(0);
12452
12453 frontend.program = program.clone();
12454 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12455 frontend.update_state_after_exec(outcome, true);
12456 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12457 let sketch_id = sketch_object.id;
12458 let sketch = expect_sketch(sketch_object);
12459 let point0_id = *sketch.segments.first().unwrap();
12460 let point1_id = *sketch.segments.get(1).unwrap();
12461 let label_position = Point2d {
12462 x: Number {
12463 value: 10.0,
12464 units: NumericSuffix::Mm,
12465 },
12466 y: Number {
12467 value: 11.0,
12468 units: NumericSuffix::Mm,
12469 },
12470 };
12471
12472 let constraint = Constraint::VerticalDistance(Distance {
12473 segments: vec![point0_id.into(), point1_id.into()],
12474 distance: Number {
12475 value: 2.0,
12476 units: NumericSuffix::Mm,
12477 },
12478 label_position: Some(label_position.clone()),
12479 source: Default::default(),
12480 });
12481 let (src_delta, scene_delta) = frontend
12482 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12483 .await
12484 .unwrap();
12485 insta::assert_snapshot!("test_vertical_distance_two_points", src_delta.text.as_str());
12486 assert_eq!(
12487 scene_delta.new_graph.objects.len(),
12488 5,
12489 "{:#?}",
12490 scene_delta.new_graph.objects
12491 );
12492 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
12493 let sketch = expect_sketch(sketch_object);
12494 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
12495 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12496 panic!("Expected constraint object");
12497 };
12498 let Constraint::VerticalDistance(distance) = constraint else {
12499 panic!("Expected vertical distance constraint");
12500 };
12501 assert_eq!(distance.label_position, Some(label_position));
12502
12503 mock_ctx.close().await;
12504 }
12505
12506 #[tokio::test(flavor = "multi_thread")]
12507 async fn test_add_fixed_standalone_point() {
12508 let initial_source = "\
12509sketch(on = XY) {
12510 point(at = [var 1, var 2])
12511}
12512";
12513
12514 let program = Program::parse(initial_source).unwrap().0.unwrap();
12515
12516 let mut frontend = FrontendState::new();
12517
12518 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
12519 let mock_ctx = ExecutorContext::new_mock(None).await;
12520 let version = Version(0);
12521
12522 frontend.hack_set_program(&ctx, program).await.unwrap();
12523 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12524 let sketch_id = sketch_object.id;
12525 let sketch = expect_sketch(sketch_object);
12526 let point_id = *sketch.segments.first().unwrap();
12527
12528 let (src_delta, scene_delta) = frontend
12529 .add_constraint(
12530 &mock_ctx,
12531 version,
12532 sketch_id,
12533 Constraint::Fixed(Fixed {
12534 points: vec![FixedPoint {
12535 point: point_id,
12536 position: Point2d {
12537 x: Number {
12538 value: 2.0,
12539 units: NumericSuffix::Mm,
12540 },
12541 y: Number {
12542 value: 3.0,
12543 units: NumericSuffix::Mm,
12544 },
12545 },
12546 }],
12547 }),
12548 )
12549 .await
12550 .unwrap();
12551 insta::assert_snapshot!("test_add_fixed_standalone_point", src_delta.text.as_str());
12552 assert_eq!(
12553 scene_delta.new_graph.objects.len(),
12554 4,
12555 "{:#?}",
12556 scene_delta.new_graph.objects
12557 );
12558
12559 ctx.close().await;
12560 mock_ctx.close().await;
12561 }
12562
12563 #[tokio::test(flavor = "multi_thread")]
12564 async fn test_add_fixed_multiple_points() {
12565 let initial_source = "\
12566sketch(on = XY) {
12567 point(at = [var 1, var 2])
12568 point(at = [var 3, var 4])
12569}
12570";
12571
12572 let program = Program::parse(initial_source).unwrap().0.unwrap();
12573
12574 let mut frontend = FrontendState::new();
12575
12576 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
12577 let mock_ctx = ExecutorContext::new_mock(None).await;
12578 let version = Version(0);
12579
12580 frontend.hack_set_program(&ctx, program).await.unwrap();
12581 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12582 let sketch_id = sketch_object.id;
12583 let sketch = expect_sketch(sketch_object);
12584 let point0_id = *sketch.segments.first().unwrap();
12585 let point1_id = *sketch.segments.get(1).unwrap();
12586
12587 let (src_delta, scene_delta) = frontend
12588 .add_constraint(
12589 &mock_ctx,
12590 version,
12591 sketch_id,
12592 Constraint::Fixed(Fixed {
12593 points: vec![
12594 FixedPoint {
12595 point: point0_id,
12596 position: Point2d {
12597 x: Number {
12598 value: 2.0,
12599 units: NumericSuffix::Mm,
12600 },
12601 y: Number {
12602 value: 3.0,
12603 units: NumericSuffix::Mm,
12604 },
12605 },
12606 },
12607 FixedPoint {
12608 point: point1_id,
12609 position: Point2d {
12610 x: Number {
12611 value: 4.0,
12612 units: NumericSuffix::Mm,
12613 },
12614 y: Number {
12615 value: 5.0,
12616 units: NumericSuffix::Mm,
12617 },
12618 },
12619 },
12620 ],
12621 }),
12622 )
12623 .await
12624 .unwrap();
12625 insta::assert_snapshot!("test_add_fixed_multiple_points", src_delta.text.as_str());
12626 assert_eq!(
12627 scene_delta.new_graph.objects.len(),
12628 6,
12629 "{:#?}",
12630 scene_delta.new_graph.objects
12631 );
12632
12633 ctx.close().await;
12634 mock_ctx.close().await;
12635 }
12636
12637 #[tokio::test(flavor = "multi_thread")]
12638 async fn test_add_fixed_owned_point() {
12639 let initial_source = "\
12640sketch(on = XY) {
12641 line(start = [var 1, var 2], end = [var 3, var 4])
12642}
12643";
12644
12645 let program = Program::parse(initial_source).unwrap().0.unwrap();
12646
12647 let mut frontend = FrontendState::new();
12648
12649 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
12650 let mock_ctx = ExecutorContext::new_mock(None).await;
12651 let version = Version(0);
12652
12653 frontend.hack_set_program(&ctx, program).await.unwrap();
12654 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12655 let sketch_id = sketch_object.id;
12656 let sketch = expect_sketch(sketch_object);
12657 let line_start_id = *sketch.segments.first().unwrap();
12658
12659 let (src_delta, scene_delta) = frontend
12660 .add_constraint(
12661 &mock_ctx,
12662 version,
12663 sketch_id,
12664 Constraint::Fixed(Fixed {
12665 points: vec![FixedPoint {
12666 point: line_start_id,
12667 position: Point2d {
12668 x: Number {
12669 value: 2.0,
12670 units: NumericSuffix::Mm,
12671 },
12672 y: Number {
12673 value: 3.0,
12674 units: NumericSuffix::Mm,
12675 },
12676 },
12677 }],
12678 }),
12679 )
12680 .await
12681 .unwrap();
12682 insta::assert_snapshot!("test_add_fixed_owned_point", src_delta.text.as_str());
12683 assert_eq!(
12684 scene_delta.new_graph.objects.len(),
12685 6,
12686 "{:#?}",
12687 scene_delta.new_graph.objects
12688 );
12689
12690 ctx.close().await;
12691 mock_ctx.close().await;
12692 }
12693
12694 #[tokio::test(flavor = "multi_thread")]
12695 async fn test_radius_error_cases() {
12696 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
12697 let mock_ctx = ExecutorContext::new_mock(None).await;
12698 let version = Version(0);
12699
12700 let initial_source_point = "\
12702sketch(on = XY) {
12703 point(at = [var 1, var 2])
12704}
12705";
12706 let program_point = Program::parse(initial_source_point).unwrap().0.unwrap();
12707 let mut frontend_point = FrontendState::new();
12708 frontend_point.hack_set_program(&ctx, program_point).await.unwrap();
12709 let sketch_object_point = find_first_sketch_object(&frontend_point.scene_graph).unwrap();
12710 let sketch_id_point = sketch_object_point.id;
12711 let sketch_point = expect_sketch(sketch_object_point);
12712 let point_id = *sketch_point.segments.first().unwrap();
12713
12714 let constraint_point = Constraint::Radius(Radius {
12715 arc: point_id,
12716 radius: Number {
12717 value: 5.0,
12718 units: NumericSuffix::Mm,
12719 },
12720 label_position: None,
12721 source: Default::default(),
12722 });
12723 let result_point = frontend_point
12724 .add_constraint(&mock_ctx, version, sketch_id_point, constraint_point)
12725 .await;
12726 assert!(result_point.is_err(), "Single point should error for radius");
12727
12728 let initial_source_line = "\
12730sketch(on = XY) {
12731 line(start = [var 1, var 2], end = [var 3, var 4])
12732}
12733";
12734 let program_line = Program::parse(initial_source_line).unwrap().0.unwrap();
12735 let mut frontend_line = FrontendState::new();
12736 frontend_line.hack_set_program(&ctx, program_line).await.unwrap();
12737 let sketch_object_line = find_first_sketch_object(&frontend_line.scene_graph).unwrap();
12738 let sketch_id_line = sketch_object_line.id;
12739 let sketch_line = expect_sketch(sketch_object_line);
12740 let line_id = *sketch_line.segments.first().unwrap();
12741
12742 let constraint_line = Constraint::Radius(Radius {
12743 arc: line_id,
12744 radius: Number {
12745 value: 5.0,
12746 units: NumericSuffix::Mm,
12747 },
12748 label_position: None,
12749 source: Default::default(),
12750 });
12751 let result_line = frontend_line
12752 .add_constraint(&mock_ctx, version, sketch_id_line, constraint_line)
12753 .await;
12754 assert!(result_line.is_err(), "Single line segment should error for radius");
12755
12756 ctx.close().await;
12757 mock_ctx.close().await;
12758 }
12759
12760 #[tokio::test(flavor = "multi_thread")]
12761 async fn test_diameter_single_arc_segment() {
12762 let initial_source = "\
12763sketch(on = XY) {
12764 arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
12765}
12766";
12767
12768 let program = Program::parse(initial_source).unwrap().0.unwrap();
12769
12770 let mut frontend = FrontendState::new();
12771
12772 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
12773 let mock_ctx = ExecutorContext::new_mock(None).await;
12774 let version = Version(0);
12775
12776 frontend.hack_set_program(&ctx, program).await.unwrap();
12777 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12778 let sketch_id = sketch_object.id;
12779 let sketch = expect_sketch(sketch_object);
12780 let arc_id = sketch
12782 .segments
12783 .iter()
12784 .find(|&seg_id| {
12785 let obj = frontend.scene_graph.objects.get(seg_id.0);
12786 matches!(
12787 obj.map(|o| &o.kind),
12788 Some(ObjectKind::Segment {
12789 segment: Segment::Arc(_)
12790 })
12791 )
12792 })
12793 .unwrap();
12794
12795 let constraint = Constraint::Diameter(Diameter {
12796 arc: *arc_id,
12797 diameter: Number {
12798 value: 10.0,
12799 units: NumericSuffix::Mm,
12800 },
12801 label_position: None,
12802 source: Default::default(),
12803 });
12804 let (src_delta, scene_delta) = frontend
12805 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12806 .await
12807 .unwrap();
12808 insta::assert_snapshot!("test_diameter_single_arc_segment", src_delta.text.as_str());
12809 assert_eq!(
12810 scene_delta.new_graph.objects.len(),
12811 7, "{:#?}",
12813 scene_delta.new_graph.objects
12814 );
12815
12816 ctx.close().await;
12817 mock_ctx.close().await;
12818 }
12819
12820 #[tokio::test(flavor = "multi_thread")]
12821 async fn test_diameter_single_arc_segment_with_label_position() {
12822 let initial_source = "\
12823sketch(on = XY) {
12824 arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
12825}
12826";
12827
12828 let program = Program::parse(initial_source).unwrap().0.unwrap();
12829 let mut frontend = FrontendState::new();
12830 let mock_ctx = ExecutorContext::new_mock(None).await;
12831 let version = Version(0);
12832
12833 frontend.program = program.clone();
12834 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12835 frontend.update_state_after_exec(outcome, true);
12836 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12837 let sketch_id = sketch_object.id;
12838 let sketch = expect_sketch(sketch_object);
12839 let arc_id = sketch
12840 .segments
12841 .iter()
12842 .find(|&seg_id| {
12843 let obj = frontend.scene_graph.objects.get(seg_id.0);
12844 matches!(
12845 obj.map(|o| &o.kind),
12846 Some(ObjectKind::Segment {
12847 segment: Segment::Arc(_)
12848 })
12849 )
12850 })
12851 .unwrap();
12852
12853 let label_position = Point2d {
12854 x: Number {
12855 value: 10.0,
12856 units: NumericSuffix::Mm,
12857 },
12858 y: Number {
12859 value: 11.0,
12860 units: NumericSuffix::Mm,
12861 },
12862 };
12863 let constraint = Constraint::Diameter(Diameter {
12864 arc: *arc_id,
12865 diameter: Number {
12866 value: 10.0,
12867 units: NumericSuffix::Mm,
12868 },
12869 label_position: Some(label_position.clone()),
12870 source: Default::default(),
12871 });
12872 let (src_delta, scene_delta) = frontend
12873 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12874 .await
12875 .unwrap();
12876 insta::assert_snapshot!(
12877 "test_diameter_single_arc_segment_with_label_position",
12878 src_delta.text.as_str()
12879 );
12880
12881 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
12882 let sketch = expect_sketch(sketch_object);
12883 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
12884 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12885 panic!("Expected constraint object");
12886 };
12887 let Constraint::Diameter(diameter) = constraint else {
12888 panic!("Expected diameter constraint");
12889 };
12890 assert_eq!(diameter.label_position, Some(label_position));
12891
12892 mock_ctx.close().await;
12893 }
12894
12895 #[tokio::test(flavor = "multi_thread")]
12896 async fn test_edit_diameter_constraint_label_position() {
12897 let initial_source = "\
12898sketch(on = XY) {
12899 arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
12900 diameter(arc1) == 10mm
12901}
12902";
12903
12904 let program = Program::parse(initial_source).unwrap().0.unwrap();
12905 let mut frontend = FrontendState::new();
12906 let mock_ctx = ExecutorContext::new_mock(None).await;
12907 let version = Version(0);
12908
12909 frontend.program = program.clone();
12910 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12911 frontend.update_state_after_exec(outcome, true);
12912 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12913 let sketch_id = sketch_object.id;
12914 let sketch = expect_sketch(sketch_object);
12915 let constraint_id = sketch.constraints[0];
12916 let label_position = Point2d {
12917 x: Number {
12918 value: 10.0,
12919 units: NumericSuffix::Mm,
12920 },
12921 y: Number {
12922 value: 11.0,
12923 units: NumericSuffix::Mm,
12924 },
12925 };
12926
12927 let (src_delta, scene_delta) = frontend
12928 .edit_distance_constraint_label_position(
12929 &mock_ctx,
12930 version,
12931 sketch_id,
12932 constraint_id,
12933 label_position.clone(),
12934 vec![],
12935 )
12936 .await
12937 .unwrap();
12938 insta::assert_snapshot!("test_edit_diameter_constraint_label_position", src_delta.text.as_str());
12939
12940 let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
12941 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12942 panic!("Expected constraint object");
12943 };
12944 let Constraint::Diameter(diameter) = constraint else {
12945 panic!("Expected diameter constraint");
12946 };
12947 assert_eq!(diameter.label_position, Some(label_position));
12948
12949 mock_ctx.close().await;
12950 }
12951
12952 #[tokio::test(flavor = "multi_thread")]
12953 async fn test_diameter_error_cases() {
12954 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
12955 let mock_ctx = ExecutorContext::new_mock(None).await;
12956 let version = Version(0);
12957
12958 let initial_source_point = "\
12960sketch(on = XY) {
12961 point(at = [var 1, var 2])
12962}
12963";
12964 let program_point = Program::parse(initial_source_point).unwrap().0.unwrap();
12965 let mut frontend_point = FrontendState::new();
12966 frontend_point.hack_set_program(&ctx, program_point).await.unwrap();
12967 let sketch_object_point = find_first_sketch_object(&frontend_point.scene_graph).unwrap();
12968 let sketch_id_point = sketch_object_point.id;
12969 let sketch_point = expect_sketch(sketch_object_point);
12970 let point_id = *sketch_point.segments.first().unwrap();
12971
12972 let constraint_point = Constraint::Diameter(Diameter {
12973 arc: point_id,
12974 diameter: Number {
12975 value: 10.0,
12976 units: NumericSuffix::Mm,
12977 },
12978 label_position: None,
12979 source: Default::default(),
12980 });
12981 let result_point = frontend_point
12982 .add_constraint(&mock_ctx, version, sketch_id_point, constraint_point)
12983 .await;
12984 assert!(result_point.is_err(), "Single point should error for diameter");
12985
12986 let initial_source_line = "\
12988sketch(on = XY) {
12989 line(start = [var 1, var 2], end = [var 3, var 4])
12990}
12991";
12992 let program_line = Program::parse(initial_source_line).unwrap().0.unwrap();
12993 let mut frontend_line = FrontendState::new();
12994 frontend_line.hack_set_program(&ctx, program_line).await.unwrap();
12995 let sketch_object_line = find_first_sketch_object(&frontend_line.scene_graph).unwrap();
12996 let sketch_id_line = sketch_object_line.id;
12997 let sketch_line = expect_sketch(sketch_object_line);
12998 let line_id = *sketch_line.segments.first().unwrap();
12999
13000 let constraint_line = Constraint::Diameter(Diameter {
13001 arc: line_id,
13002 diameter: Number {
13003 value: 10.0,
13004 units: NumericSuffix::Mm,
13005 },
13006 label_position: None,
13007 source: Default::default(),
13008 });
13009 let result_line = frontend_line
13010 .add_constraint(&mock_ctx, version, sketch_id_line, constraint_line)
13011 .await;
13012 assert!(result_line.is_err(), "Single line segment should error for diameter");
13013
13014 ctx.close().await;
13015 mock_ctx.close().await;
13016 }
13017
13018 #[tokio::test(flavor = "multi_thread")]
13019 async fn test_line_horizontal() {
13020 let initial_source = "\
13021sketch(on = XY) {
13022 line(start = [var 1, var 2], end = [var 3, var 4])
13023}
13024";
13025
13026 let program = Program::parse(initial_source).unwrap().0.unwrap();
13027
13028 let mut frontend = FrontendState::new();
13029
13030 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13031 let mock_ctx = ExecutorContext::new_mock(None).await;
13032 let version = Version(0);
13033
13034 frontend.hack_set_program(&ctx, program).await.unwrap();
13035 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13036 let sketch_id = sketch_object.id;
13037 let sketch = expect_sketch(sketch_object);
13038 let line1_id = *sketch.segments.get(2).unwrap();
13039
13040 let constraint = Constraint::Horizontal(Horizontal::Line { line: line1_id });
13041 let (src_delta, scene_delta) = frontend
13042 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13043 .await
13044 .unwrap();
13045 insta::assert_snapshot!("test_line_horizontal", src_delta.text.as_str());
13046 assert_eq!(
13047 scene_delta.new_graph.objects.len(),
13048 6,
13049 "{:#?}",
13050 scene_delta.new_graph.objects
13051 );
13052
13053 ctx.close().await;
13054 mock_ctx.close().await;
13055 }
13056
13057 #[tokio::test(flavor = "multi_thread")]
13058 async fn test_control_point_spline_edge_horizontal() {
13059 let initial_source = "\
13060@settings(experimentalFeatures = allow)
13061splineSketch = sketch(on = XY) {
13062 controlPointSpline1 = controlPointSpline(points = [
13063 [var 0mm, var 0mm],
13064 [var 10mm, var 20mm],
13065 [var 20mm, var 0mm],
13066 ])
13067}
13068";
13069
13070 let program = Program::parse(initial_source).unwrap().0.unwrap();
13071
13072 let mut frontend = FrontendState::new();
13073
13074 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13075 let mock_ctx = ExecutorContext::new_mock(None).await;
13076 let version = Version(0);
13077
13078 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
13079 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13080 let sketch_id = sketch_object.id;
13081 let sketch = expect_sketch(sketch_object);
13082 let spline_id = sketch
13083 .segments
13084 .iter()
13085 .copied()
13086 .find(|seg_id| {
13087 matches!(
13088 &frontend.scene_graph.objects[seg_id.0].kind,
13089 ObjectKind::Segment {
13090 segment: Segment::ControlPointSpline(_)
13091 }
13092 )
13093 })
13094 .expect("Expected a control point spline segment in sketch");
13095 let edge_id = frontend
13096 .scene_graph
13097 .objects
13098 .iter()
13099 .find_map(|obj| match &obj.kind {
13100 ObjectKind::Segment {
13101 segment: Segment::Line(line),
13102 } if line.owner == Some(spline_id) => Some(obj.id),
13103 _ => None,
13104 })
13105 .expect("Expected an owned control-polygon edge");
13106
13107 let constraint = Constraint::Horizontal(Horizontal::Line { line: edge_id });
13108 let (src_delta, _) = frontend
13109 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13110 .await
13111 .unwrap();
13112 assert!(
13113 src_delta.text.contains("horizontal(controlPointSpline1.edges[0])"),
13114 "Expected horizontal constraint on spline edge, got: {}",
13115 src_delta.text
13116 );
13117
13118 ctx.close().await;
13119 mock_ctx.close().await;
13120 }
13121
13122 #[tokio::test(flavor = "multi_thread")]
13123 async fn test_control_point_spline_edge_angle() {
13124 let initial_source = "\
13125@settings(experimentalFeatures = allow)
13126splineSketch = sketch(on = XY) {
13127 controlPointSpline1 = controlPointSpline(points = [
13128 [var 0mm, var 0mm],
13129 [var 10mm, var 20mm],
13130 [var 20mm, var 0mm],
13131 ])
13132
13133 line1 = line(start = [var 40mm, var 0mm], end = [var 60mm, var 10mm])
13134}
13135";
13136
13137 let program = Program::parse(initial_source).unwrap().0.unwrap();
13138
13139 let mut frontend = FrontendState::new();
13140
13141 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13142 let mock_ctx = ExecutorContext::new_mock(None).await;
13143 let version = Version(0);
13144
13145 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
13146 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13147 let sketch_id = sketch_object.id;
13148 let sketch = expect_sketch(sketch_object);
13149 let spline_id = sketch
13150 .segments
13151 .iter()
13152 .copied()
13153 .find(|seg_id| {
13154 matches!(
13155 &frontend.scene_graph.objects[seg_id.0].kind,
13156 ObjectKind::Segment {
13157 segment: Segment::ControlPointSpline(_)
13158 }
13159 )
13160 })
13161 .expect("Expected a control point spline segment in sketch");
13162 let edge_id = frontend
13163 .scene_graph
13164 .objects
13165 .iter()
13166 .find_map(|obj| match &obj.kind {
13167 ObjectKind::Segment {
13168 segment: Segment::Line(line),
13169 } if line.owner == Some(spline_id) => Some(obj.id),
13170 _ => None,
13171 })
13172 .expect("Expected an owned control-polygon edge");
13173 let line1_id = frontend
13174 .scene_graph
13175 .objects
13176 .iter()
13177 .find_map(|obj| match &obj.kind {
13178 ObjectKind::Segment {
13179 segment: Segment::Line(line),
13180 } if line.owner.is_none() && obj.label == "line1" => Some(obj.id),
13181 _ => None,
13182 })
13183 .or_else(|| {
13184 sketch.segments.iter().copied().find(|seg_id| {
13185 matches!(
13186 &frontend.scene_graph.objects[seg_id.0].kind,
13187 ObjectKind::Segment {
13188 segment: Segment::Line(line),
13189 } if line.owner.is_none()
13190 )
13191 })
13192 })
13193 .expect("Expected a standalone line segment in sketch");
13194
13195 let constraint = Constraint::Angle(Angle {
13196 lines: vec![line1_id, edge_id],
13197 angle: Number {
13198 value: 30.0,
13199 units: NumericSuffix::Deg,
13200 },
13201 sector: None,
13202 inverse: None,
13203 label_position: None,
13204 source: Default::default(),
13205 });
13206 let (src_delta, _) = frontend
13207 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13208 .await
13209 .unwrap();
13210 assert!(
13211 src_delta
13212 .text
13213 .contains("angle([line1, controlPointSpline1.edges[0]]) == 30deg"),
13214 "Expected angle constraint on spline edge, got: {}",
13215 src_delta.text
13216 );
13217
13218 ctx.close().await;
13219 mock_ctx.close().await;
13220 }
13221
13222 #[tokio::test(flavor = "multi_thread")]
13223 async fn test_ui_scene_graph_hides_same_spline_coincident_constraints() {
13224 let initial_source = "\
13225@settings(experimentalFeatures = allow)
13226splineSketch = sketch(on = XY) {
13227 spline1 = controlPointSpline(points = [
13228 [var 0mm, var 0mm],
13229 [var 10mm, var 20mm],
13230 [var 20mm, var 0mm],
13231 ])
13232 line1 = line(start = [var 0mm, var 0mm], end = [var -10mm, var 0mm])
13233 coincident([spline1.controls[1], spline1.edges[0]])
13234 coincident([spline1.controls[0], line1])
13235}
13236";
13237
13238 let program = Program::parse(initial_source).unwrap().0.unwrap();
13239
13240 let mut frontend = FrontendState::new();
13241
13242 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13243 let mock_ctx = ExecutorContext::new_mock(None).await;
13244
13245 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
13246
13247 let ui_scene_graph = frontend.scene_graph_for_ui();
13248 let sketch_object = find_first_sketch_object(&ui_scene_graph).unwrap();
13249 let sketch = expect_sketch(sketch_object);
13250
13251 assert_eq!(
13252 sketch.constraints.len(),
13253 1,
13254 "Expected only the external coincident constraint to remain visible in the UI scene graph"
13255 );
13256
13257 let visible_constraints = ui_scene_graph
13258 .objects
13259 .iter()
13260 .filter_map(|object| match &object.kind {
13261 ObjectKind::Constraint {
13262 constraint: Constraint::Coincident(coincident),
13263 } => Some(coincident.clone()),
13264 _ => None,
13265 })
13266 .collect::<Vec<_>>();
13267
13268 assert_eq!(
13269 visible_constraints.len(),
13270 1,
13271 "Expected only one coincident constraint object in the UI scene graph"
13272 );
13273 assert_eq!(
13274 visible_constraints[0].get_segments().len(),
13275 2,
13276 "Expected the remaining visible coincident constraint to reference two segments"
13277 );
13278
13279 ctx.close().await;
13280 mock_ctx.close().await;
13281 }
13282
13283 #[tokio::test(flavor = "multi_thread")]
13284 async fn test_edit_control_point_spline_can_append_control_point() {
13285 let initial_source = "\
13286@settings(experimentalFeatures = allow)
13287splineSketch = sketch(on = XY) {
13288 controlPointSpline(points = [
13289 [var 0mm, var 0mm],
13290 [var 10mm, var 20mm],
13291 [var 20mm, var 0mm],
13292 ])
13293}
13294";
13295
13296 let program = Program::parse(initial_source).unwrap().0.unwrap();
13297
13298 let mut frontend = FrontendState::new();
13299
13300 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13301 let mock_ctx = ExecutorContext::new_mock(None).await;
13302 let version = Version(0);
13303
13304 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
13305 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13306 let sketch_id = sketch_object.id;
13307 let sketch = expect_sketch(sketch_object);
13308 let spline_id = sketch
13309 .segments
13310 .iter()
13311 .copied()
13312 .find(|seg_id| {
13313 matches!(
13314 &frontend.scene_graph.objects[seg_id.0].kind,
13315 ObjectKind::Segment {
13316 segment: Segment::ControlPointSpline(_)
13317 }
13318 )
13319 })
13320 .expect("Expected a control point spline segment in sketch");
13321
13322 let ctor = ControlPointSplineCtor {
13323 points: vec![
13324 Point2d {
13325 x: Expr::Var(Number {
13326 value: 0.0,
13327 units: NumericSuffix::Mm,
13328 }),
13329 y: Expr::Var(Number {
13330 value: 0.0,
13331 units: NumericSuffix::Mm,
13332 }),
13333 },
13334 Point2d {
13335 x: Expr::Var(Number {
13336 value: 10.0,
13337 units: NumericSuffix::Mm,
13338 }),
13339 y: Expr::Var(Number {
13340 value: 20.0,
13341 units: NumericSuffix::Mm,
13342 }),
13343 },
13344 Point2d {
13345 x: Expr::Var(Number {
13346 value: 20.0,
13347 units: NumericSuffix::Mm,
13348 }),
13349 y: Expr::Var(Number {
13350 value: 0.0,
13351 units: NumericSuffix::Mm,
13352 }),
13353 },
13354 Point2d {
13355 x: Expr::Var(Number {
13356 value: 30.0,
13357 units: NumericSuffix::Mm,
13358 }),
13359 y: Expr::Var(Number {
13360 value: 10.0,
13361 units: NumericSuffix::Mm,
13362 }),
13363 },
13364 ],
13365 construction: None,
13366 };
13367
13368 let segments = vec![ExistingSegmentCtor {
13369 id: spline_id,
13370 ctor: SegmentCtor::ControlPointSpline(ctor),
13371 }];
13372 let (src_delta, scene_delta) = frontend
13373 .edit_segments(&mock_ctx, version, sketch_id, segments)
13374 .await
13375 .unwrap();
13376
13377 assert!(
13378 src_delta.text.contains("[var 30mm, var 10mm]"),
13379 "Expected appended spline control point in source, got: {}",
13380 src_delta.text
13381 );
13382
13383 assert!(
13384 scene_delta.invalidates_ids,
13385 "Expected appending a spline control point to invalidate ids"
13386 );
13387 let updated_spline = scene_delta
13388 .new_graph
13389 .objects
13390 .iter()
13391 .find_map(|obj| match &obj.kind {
13392 ObjectKind::Segment {
13393 segment: Segment::ControlPointSpline(updated_spline),
13394 } if updated_spline.controls.len() == 4 => Some(updated_spline),
13395 _ => None,
13396 })
13397 .expect("Expected edited scene graph to contain a four-point control point spline");
13398 assert_eq!(
13399 updated_spline.controls.len(),
13400 4,
13401 "Expected edited spline to expose four control points"
13402 );
13403
13404 ctx.close().await;
13405 mock_ctx.close().await;
13406 }
13407
13408 #[tokio::test(flavor = "multi_thread")]
13409 async fn test_line_vertical() {
13410 let initial_source = "\
13411sketch(on = XY) {
13412 line(start = [var 1, var 2], end = [var 3, var 4])
13413}
13414";
13415
13416 let program = Program::parse(initial_source).unwrap().0.unwrap();
13417
13418 let mut frontend = FrontendState::new();
13419
13420 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13421 let mock_ctx = ExecutorContext::new_mock(None).await;
13422 let version = Version(0);
13423
13424 frontend.hack_set_program(&ctx, program).await.unwrap();
13425 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13426 let sketch_id = sketch_object.id;
13427 let sketch = expect_sketch(sketch_object);
13428 let line1_id = *sketch.segments.get(2).unwrap();
13429
13430 let constraint = Constraint::Vertical(Vertical::Line { line: line1_id });
13431 let (src_delta, scene_delta) = frontend
13432 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13433 .await
13434 .unwrap();
13435 insta::assert_snapshot!("test_line_vertical", src_delta.text.as_str());
13436 assert_eq!(
13437 scene_delta.new_graph.objects.len(),
13438 6,
13439 "{:#?}",
13440 scene_delta.new_graph.objects
13441 );
13442
13443 ctx.close().await;
13444 mock_ctx.close().await;
13445 }
13446
13447 #[tokio::test(flavor = "multi_thread")]
13448 async fn test_points_vertical() {
13449 let initial_source = "\
13450sketch001 = sketch(on = XY) {
13451 p0 = point(at = [var -2.23mm, var 3.1mm])
13452 pf = point(at = [4, 4])
13453}
13454";
13455
13456 let program = Program::parse(initial_source).unwrap().0.unwrap();
13457
13458 let mut frontend = FrontendState::new();
13459
13460 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13461 let mock_ctx = ExecutorContext::new_mock(None).await;
13462 let version = Version(0);
13463
13464 frontend.hack_set_program(&ctx, program).await.unwrap();
13465 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13466 let sketch_id = sketch_object.id;
13467 let sketch = expect_sketch(sketch_object);
13468 let point_ids = vec![
13469 sketch.segments.first().unwrap().to_owned(),
13470 sketch.segments.get(1).unwrap().to_owned(),
13471 ];
13472
13473 let constraint = Constraint::Vertical(Vertical::Points {
13474 points: point_ids.into_iter().map(ConstraintSegment::from).collect(),
13475 });
13476 let (src_delta, scene_delta) = frontend
13477 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13478 .await
13479 .unwrap();
13480 insta::assert_snapshot!("test_points_vertical", src_delta.text.as_str());
13481 assert_eq!(
13482 scene_delta.new_graph.objects.len(),
13483 5,
13484 "{:#?}",
13485 scene_delta.new_graph.objects
13486 );
13487
13488 ctx.close().await;
13489 mock_ctx.close().await;
13490 }
13491
13492 #[tokio::test(flavor = "multi_thread")]
13493 async fn test_points_horizontal() {
13494 let initial_source = "\
13495sketch001 = sketch(on = XY) {
13496 p0 = point(at = [var -2.23mm, var 3.1mm])
13497 pf = point(at = [4, 4])
13498}
13499";
13500
13501 let program = Program::parse(initial_source).unwrap().0.unwrap();
13502
13503 let mut frontend = FrontendState::new();
13504
13505 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13506 let mock_ctx = ExecutorContext::new_mock(None).await;
13507 let version = Version(0);
13508
13509 frontend.hack_set_program(&ctx, program).await.unwrap();
13510 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13511 let sketch_id = sketch_object.id;
13512 let sketch = expect_sketch(sketch_object);
13513 let point_ids = vec![
13514 sketch.segments.first().unwrap().to_owned(),
13515 sketch.segments.get(1).unwrap().to_owned(),
13516 ];
13517
13518 let constraint = Constraint::Horizontal(Horizontal::Points {
13519 points: point_ids.into_iter().map(ConstraintSegment::from).collect(),
13520 });
13521 let (src_delta, scene_delta) = frontend
13522 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13523 .await
13524 .unwrap();
13525 insta::assert_snapshot!("test_points_horizontal", src_delta.text.as_str());
13526 assert_eq!(
13527 scene_delta.new_graph.objects.len(),
13528 5,
13529 "{:#?}",
13530 scene_delta.new_graph.objects
13531 );
13532
13533 ctx.close().await;
13534 mock_ctx.close().await;
13535 }
13536
13537 #[tokio::test(flavor = "multi_thread")]
13538 async fn test_point_horizontal_with_origin() {
13539 let initial_source = "\
13540sketch001 = sketch(on = XY) {
13541 p0 = point(at = [var -2.23mm, var 3.1mm])
13542}
13543";
13544
13545 let program = Program::parse(initial_source).unwrap().0.unwrap();
13546
13547 let mut frontend = FrontendState::new();
13548
13549 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13550 let mock_ctx = ExecutorContext::new_mock(None).await;
13551 let version = Version(0);
13552
13553 frontend.hack_set_program(&ctx, program).await.unwrap();
13554 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13555 let sketch_id = sketch_object.id;
13556 let sketch = expect_sketch(sketch_object);
13557 let point_id = *sketch.segments.first().unwrap();
13558
13559 let constraint = Constraint::Horizontal(Horizontal::Points {
13560 points: vec![ConstraintSegment::from(point_id), ConstraintSegment::ORIGIN],
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_point_horizontal_with_origin", src_delta.text.as_str());
13567 assert_eq!(
13568 scene_delta.new_graph.objects.len(),
13569 4,
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_lines_equal_length() {
13580 let initial_source = "\
13581sketch(on = XY) {
13582 line(start = [var 1, var 2], end = [var 3, var 4])
13583 line(start = [var 5, var 6], end = [var 7, var 8])
13584}
13585";
13586
13587 let program = Program::parse(initial_source).unwrap().0.unwrap();
13588
13589 let mut frontend = FrontendState::new();
13590
13591 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13592 let mock_ctx = ExecutorContext::new_mock(None).await;
13593 let version = Version(0);
13594
13595 frontend.hack_set_program(&ctx, program).await.unwrap();
13596 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13597 let sketch_id = sketch_object.id;
13598 let sketch = expect_sketch(sketch_object);
13599 let line1_id = *sketch.segments.get(2).unwrap();
13600 let line2_id = *sketch.segments.get(5).unwrap();
13601
13602 let constraint = Constraint::LinesEqualLength(LinesEqualLength {
13603 lines: vec![line1_id, line2_id],
13604 });
13605 let (src_delta, scene_delta) = frontend
13606 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13607 .await
13608 .unwrap();
13609 insta::assert_snapshot!("test_lines_equal_length", src_delta.text.as_str());
13610 assert_eq!(
13611 scene_delta.new_graph.objects.len(),
13612 9,
13613 "{:#?}",
13614 scene_delta.new_graph.objects
13615 );
13616
13617 ctx.close().await;
13618 mock_ctx.close().await;
13619 }
13620
13621 #[tokio::test(flavor = "multi_thread")]
13622 async fn test_add_constraint_multi_line_equal_length() {
13623 let initial_source = "\
13624sketch(on = XY) {
13625 line(start = [var 1, var 2], end = [var 3, var 4])
13626 line(start = [var 5, var 6], end = [var 7, var 8])
13627 line(start = [var 9, var 10], end = [var 11, var 12])
13628}
13629";
13630
13631 let program = Program::parse(initial_source).unwrap().0.unwrap();
13632
13633 let mut frontend = FrontendState::new();
13634 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13635 let mock_ctx = ExecutorContext::new_mock(None).await;
13636 let version = Version(0);
13637
13638 frontend.hack_set_program(&ctx, program).await.unwrap();
13639 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13640 let sketch_id = sketch_object.id;
13641 let sketch = expect_sketch(sketch_object);
13642 let line1_id = *sketch.segments.get(2).unwrap();
13643 let line2_id = *sketch.segments.get(5).unwrap();
13644 let line3_id = *sketch.segments.get(8).unwrap();
13645
13646 let constraint = Constraint::LinesEqualLength(LinesEqualLength {
13647 lines: vec![line1_id, line2_id, line3_id],
13648 });
13649 let (src_delta, scene_delta) = frontend
13650 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13651 .await
13652 .unwrap();
13653 insta::assert_snapshot!("test_add_constraint_multi_line_equal_length", src_delta.text.as_str());
13654 let constraints = scene_delta
13655 .new_graph
13656 .objects
13657 .iter()
13658 .filter_map(|obj| {
13659 let ObjectKind::Constraint { constraint } = &obj.kind else {
13660 return None;
13661 };
13662 Some(constraint)
13663 })
13664 .collect::<Vec<_>>();
13665
13666 assert_eq!(constraints.len(), 1, "{:#?}", frontend.scene_graph.objects);
13667 let Constraint::LinesEqualLength(lines_equal_length) = constraints[0] else {
13668 panic!("expected equal length constraint, got {:?}", constraints[0]);
13669 };
13670 assert_eq!(lines_equal_length.lines.len(), 3);
13671
13672 ctx.close().await;
13673 mock_ctx.close().await;
13674 }
13675
13676 #[tokio::test(flavor = "multi_thread")]
13677 async fn test_lines_parallel() {
13678 let initial_source = "\
13679sketch(on = XY) {
13680 line(start = [var 1, var 2], end = [var 3, var 4])
13681 line(start = [var 5, var 6], end = [var 7, var 8])
13682}
13683";
13684
13685 let program = Program::parse(initial_source).unwrap().0.unwrap();
13686
13687 let mut frontend = FrontendState::new();
13688
13689 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13690 let mock_ctx = ExecutorContext::new_mock(None).await;
13691 let version = Version(0);
13692
13693 frontend.hack_set_program(&ctx, program).await.unwrap();
13694 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13695 let sketch_id = sketch_object.id;
13696 let sketch = expect_sketch(sketch_object);
13697 let line1_id = *sketch.segments.get(2).unwrap();
13698 let line2_id = *sketch.segments.get(5).unwrap();
13699
13700 let constraint = Constraint::Parallel(Parallel {
13701 lines: vec![line1_id, line2_id],
13702 });
13703 let (src_delta, scene_delta) = frontend
13704 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13705 .await
13706 .unwrap();
13707 insta::assert_snapshot!("test_lines_parallel", src_delta.text.as_str());
13708 assert_eq!(
13709 scene_delta.new_graph.objects.len(),
13710 9,
13711 "{:#?}",
13712 scene_delta.new_graph.objects
13713 );
13714
13715 ctx.close().await;
13716 mock_ctx.close().await;
13717 }
13718
13719 #[tokio::test(flavor = "multi_thread")]
13720 async fn test_lines_parallel_multiline() {
13721 let initial_source = "\
13722sketch(on = XY) {
13723 line(start = [var 1, var 2], end = [var 3, var 4])
13724 line(start = [var 5, var 6], end = [var 7, var 8])
13725 line(start = [var 9, var 10], end = [var 11, var 12])
13726}
13727";
13728
13729 let program = Program::parse(initial_source).unwrap().0.unwrap();
13730
13731 let mut frontend = FrontendState::new();
13732
13733 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13734 let mock_ctx = ExecutorContext::new_mock(None).await;
13735 let version = Version(0);
13736
13737 frontend.hack_set_program(&ctx, program).await.unwrap();
13738 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13739 let sketch_id = sketch_object.id;
13740 let sketch = expect_sketch(sketch_object);
13741 let line1_id = *sketch.segments.get(2).unwrap();
13742 let line2_id = *sketch.segments.get(5).unwrap();
13743 let line3_id = *sketch.segments.get(8).unwrap();
13744
13745 let constraint = Constraint::Parallel(Parallel {
13746 lines: vec![line1_id, line2_id, line3_id],
13747 });
13748 let (src_delta, scene_delta) = frontend
13749 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13750 .await
13751 .unwrap();
13752 insta::assert_snapshot!("test_lines_parallel_multiline", src_delta.text.as_str());
13753
13754 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
13755 let sketch = expect_sketch(sketch_object);
13756 assert_eq!(sketch.constraints.len(), 1);
13757
13758 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
13759 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
13760 panic!("Expected constraint object");
13761 };
13762 let Constraint::Parallel(parallel) = constraint else {
13763 panic!("Expected parallel constraint");
13764 };
13765 assert_eq!(parallel.lines.len(), 3);
13766
13767 ctx.close().await;
13768 mock_ctx.close().await;
13769 }
13770
13771 #[tokio::test(flavor = "multi_thread")]
13772 async fn test_lines_perpendicular() {
13773 let initial_source = "\
13774sketch(on = XY) {
13775 line(start = [var 1, var 2], end = [var 3, var 4])
13776 line(start = [var 5, var 6], end = [var 7, var 8])
13777}
13778";
13779
13780 let program = Program::parse(initial_source).unwrap().0.unwrap();
13781
13782 let mut frontend = FrontendState::new();
13783
13784 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13785 let mock_ctx = ExecutorContext::new_mock(None).await;
13786 let version = Version(0);
13787
13788 frontend.hack_set_program(&ctx, program).await.unwrap();
13789 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13790 let sketch_id = sketch_object.id;
13791 let sketch = expect_sketch(sketch_object);
13792 let line1_id = *sketch.segments.get(2).unwrap();
13793 let line2_id = *sketch.segments.get(5).unwrap();
13794
13795 let constraint = Constraint::Perpendicular(Perpendicular {
13796 lines: vec![line1_id, line2_id],
13797 });
13798 let (src_delta, scene_delta) = frontend
13799 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13800 .await
13801 .unwrap();
13802 insta::assert_snapshot!("test_lines_perpendicular", src_delta.text.as_str());
13803 assert_eq!(
13804 scene_delta.new_graph.objects.len(),
13805 9,
13806 "{:#?}",
13807 scene_delta.new_graph.objects
13808 );
13809
13810 ctx.close().await;
13811 mock_ctx.close().await;
13812 }
13813
13814 #[tokio::test(flavor = "multi_thread")]
13815 async fn test_lines_angle() {
13816 let initial_source = "\
13817sketch(on = XY) {
13818 line(start = [var 1, var 2], end = [var 3, var 4])
13819 line(start = [var 5, var 6], end = [var 7, var 8])
13820}
13821";
13822
13823 let program = Program::parse(initial_source).unwrap().0.unwrap();
13824
13825 let mut frontend = FrontendState::new();
13826
13827 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13828 let mock_ctx = ExecutorContext::new_mock(None).await;
13829 let version = Version(0);
13830
13831 frontend.hack_set_program(&ctx, program).await.unwrap();
13832 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13833 let sketch_id = sketch_object.id;
13834 let sketch = expect_sketch(sketch_object);
13835 let line1_id = *sketch.segments.get(2).unwrap();
13836 let line2_id = *sketch.segments.get(5).unwrap();
13837
13838 let constraint = Constraint::Angle(Angle {
13839 lines: vec![line1_id, line2_id],
13840 angle: Number {
13841 value: 30.0,
13842 units: NumericSuffix::Deg,
13843 },
13844 sector: None,
13845 inverse: None,
13846 label_position: None,
13847 source: Default::default(),
13848 });
13849 let (src_delta, scene_delta) = frontend
13850 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13851 .await
13852 .unwrap();
13853 insta::assert_snapshot!("test_lines_angle", src_delta.text.as_str());
13854 assert_eq!(
13855 scene_delta.new_graph.objects.len(),
13856 9,
13857 "{:#?}",
13858 scene_delta.new_graph.objects
13859 );
13860
13861 ctx.close().await;
13862 mock_ctx.close().await;
13863 }
13864
13865 #[tokio::test(flavor = "multi_thread")]
13866 async fn test_lines_angle_with_sector_uses_angle_dimension() {
13867 let initial_source = "\
13868sketch(on = XY) {
13869 line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
13870 line(start = [var 0mm, var 0mm], end = [var 0mm, var 4mm])
13871}
13872";
13873
13874 let program = Program::parse(initial_source).unwrap().0.unwrap();
13875
13876 let mut frontend = FrontendState::new();
13877
13878 let mock_ctx = ExecutorContext::new_mock(None).await;
13879 let version = Version(0);
13880
13881 frontend.program = program.clone();
13882 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
13883 frontend.update_state_after_exec(outcome, true);
13884 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13885 let sketch_id = sketch_object.id;
13886 let sketch = expect_sketch(sketch_object);
13887 let line1_id = *sketch.segments.get(2).unwrap();
13888 let line2_id = *sketch.segments.get(5).unwrap();
13889
13890 let constraint = Constraint::Angle(Angle {
13891 lines: vec![line1_id, line2_id],
13892 angle: Number {
13893 value: 270.0,
13894 units: NumericSuffix::Deg,
13895 },
13896 sector: Some(1),
13897 inverse: Some(true),
13898 label_position: Some(Point2d {
13899 x: Number {
13900 value: -0.73,
13901 units: NumericSuffix::Mm,
13902 },
13903 y: Number {
13904 value: 0.75,
13905 units: NumericSuffix::Mm,
13906 },
13907 }),
13908 source: Default::default(),
13909 });
13910 let (src_delta, _) = frontend
13911 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13912 .await
13913 .unwrap();
13914 assert_eq!(
13915 src_delta.text.as_str(),
13916 "\
13917sketch(on = XY) {
13918 line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
13919 line2 = line(start = [var 0mm, var 0mm], end = [var 0mm, var 4mm])
13920 angleDimension(
13921 lines = [line1, line2],
13922 sector = 1,
13923 inverse = true,
13924 labelPosition = [-0.73mm, 0.75mm],
13925 ) == 270deg
13926}
13927"
13928 );
13929
13930 mock_ctx.close().await;
13931 }
13932
13933 #[tokio::test(flavor = "multi_thread")]
13934 async fn test_segments_tangent() {
13935 let initial_source = "\
13936sketch(on = XY) {
13937 line(start = [var 1, var 2], end = [var 3, var 4])
13938 arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
13939}
13940";
13941
13942 let program = Program::parse(initial_source).unwrap().0.unwrap();
13943
13944 let mut frontend = FrontendState::new();
13945
13946 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13947 let mock_ctx = ExecutorContext::new_mock(None).await;
13948 let version = Version(0);
13949
13950 frontend.hack_set_program(&ctx, program).await.unwrap();
13951 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13952 let sketch_id = sketch_object.id;
13953 let sketch = expect_sketch(sketch_object);
13954 let line1_id = *sketch.segments.get(2).unwrap();
13955 let arc1_id = *sketch.segments.get(6).unwrap();
13956
13957 let constraint = Constraint::Tangent(Tangent {
13958 input: vec![line1_id, arc1_id],
13959 });
13960 let (src_delta, scene_delta) = frontend
13961 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13962 .await
13963 .unwrap();
13964 insta::assert_snapshot!("test_segments_tangent", src_delta.text.as_str());
13965 assert_eq!(
13966 scene_delta.new_graph.objects.len(),
13967 10,
13968 "{:#?}",
13969 scene_delta.new_graph.objects
13970 );
13971
13972 ctx.close().await;
13973 mock_ctx.close().await;
13974 }
13975
13976 #[tokio::test(flavor = "multi_thread")]
13977 async fn test_point_midpoint() {
13978 let initial_source = "\
13979sketch(on = XY) {
13980 point(at = [var 1, var 1])
13981 line(start = [var 0, var 0], end = [var 6, var 4])
13982}
13983";
13984
13985 let program = Program::parse(initial_source).unwrap().0.unwrap();
13986
13987 let mut frontend = FrontendState::new();
13988
13989 let ctx = ExecutorContext::new_mock(None).await;
13990 let version = Version(0);
13991
13992 frontend.program = program.clone();
13993 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
13994 frontend.update_state_after_exec(outcome, true);
13995 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13996 let sketch_id = sketch_object.id;
13997 let sketch = expect_sketch(sketch_object);
13998 let point_id = *sketch.segments.first().unwrap();
13999 let line_id = *sketch.segments.get(3).unwrap();
14000
14001 let constraint = Constraint::Midpoint(Midpoint {
14002 point: ConstraintSegment::from(point_id),
14003 segment: line_id,
14004 });
14005 let (src_delta, scene_delta) = frontend
14006 .add_constraint(&ctx, version, sketch_id, constraint)
14007 .await
14008 .unwrap();
14009 insta::assert_snapshot!("test_point_midpoint", src_delta.text.as_str());
14010 assert_eq!(
14011 scene_delta.new_graph.objects.len(),
14012 7,
14013 "{:#?}",
14014 scene_delta.new_graph.objects
14015 );
14016
14017 ctx.close().await;
14018 }
14019
14020 #[tokio::test(flavor = "multi_thread")]
14021 async fn test_segments_symmetric() {
14022 let initial_source = "\
14023sketch(on = XY) {
14024 line(start = [var 0, var 0], end = [var 0, var 4])
14025 line(start = [var 4, var 0], end = [var 4, var 4])
14026 line(start = [var 2, var -1], end = [var 2, var 5])
14027}
14028";
14029
14030 let program = Program::parse(initial_source).unwrap().0.unwrap();
14031
14032 let mut frontend = FrontendState::new();
14033
14034 let ctx = ExecutorContext::new_mock(None).await;
14035 let version = Version(0);
14036
14037 frontend.program = program.clone();
14038 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14039 frontend.update_state_after_exec(outcome, true);
14040 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14041 let sketch_id = sketch_object.id;
14042 let sketch = expect_sketch(sketch_object);
14043 let line1_id = *sketch.segments.get(2).unwrap();
14044 let line2_id = *sketch.segments.get(5).unwrap();
14045 let axis_id = *sketch.segments.get(8).unwrap();
14046
14047 let constraint = Constraint::Symmetric(Symmetric {
14048 input: vec![line1_id, line2_id],
14049 axis: axis_id,
14050 });
14051 let (src_delta, scene_delta) = frontend
14052 .add_constraint(&ctx, version, sketch_id, constraint)
14053 .await
14054 .unwrap();
14055 insta::assert_snapshot!("test_segments_symmetric", src_delta.text.as_str());
14056 assert_eq!(
14057 scene_delta.new_graph.objects.len(),
14058 12,
14059 "{:#?}",
14060 scene_delta.new_graph.objects
14061 );
14062
14063 ctx.close().await;
14064 }
14065
14066 #[tokio::test(flavor = "multi_thread")]
14067 async fn test_point_arc_midpoint() {
14068 let initial_source = "\
14069sketch(on = XY) {
14070 point(at = [var 6, var 3])
14071 arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
14072}
14073";
14074
14075 let program = Program::parse(initial_source).unwrap().0.unwrap();
14076
14077 let mut frontend = FrontendState::new();
14078
14079 let ctx = ExecutorContext::new_mock(None).await;
14080 let version = Version(0);
14081
14082 frontend.program = program.clone();
14083 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14084 frontend.update_state_after_exec(outcome, true);
14085 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14086 let sketch_id = sketch_object.id;
14087 let sketch = expect_sketch(sketch_object);
14088 let point_id = *sketch.segments.first().unwrap();
14089 let arc_id = *sketch.segments.get(4).unwrap();
14090
14091 let constraint = Constraint::Midpoint(Midpoint {
14092 point: ConstraintSegment::from(point_id),
14093 segment: arc_id,
14094 });
14095 let (src_delta, scene_delta) = frontend
14096 .add_constraint(&ctx, version, sketch_id, constraint)
14097 .await
14098 .unwrap();
14099 insta::assert_snapshot!("test_point_arc_midpoint", src_delta.text.as_str());
14100 assert_eq!(
14101 scene_delta.new_graph.objects.len(),
14102 8,
14103 "{:#?}",
14104 scene_delta.new_graph.objects
14105 );
14106
14107 ctx.close().await;
14108 }
14109
14110 #[tokio::test(flavor = "multi_thread")]
14111 async fn test_origin_line_midpoint() {
14112 let initial_source = "\
14113sketch(on = XY) {
14114 line(start = [var 0, var 0], end = [var 6, var 4])
14115}
14116";
14117
14118 let program = Program::parse(initial_source).unwrap().0.unwrap();
14119
14120 let mut frontend = FrontendState::new();
14121
14122 let ctx = ExecutorContext::new_mock(None).await;
14123 let version = Version(0);
14124
14125 frontend.program = program.clone();
14126 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14127 frontend.update_state_after_exec(outcome, true);
14128 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14129 let sketch_id = sketch_object.id;
14130 let sketch = expect_sketch(sketch_object);
14131 let line_id = *sketch.segments.get(2).unwrap();
14132
14133 let constraint = Constraint::Midpoint(Midpoint {
14134 point: ConstraintSegment::ORIGIN,
14135 segment: line_id,
14136 });
14137 let (src_delta, scene_delta) = frontend
14138 .add_constraint(&ctx, version, sketch_id, constraint)
14139 .await
14140 .unwrap();
14141 insta::assert_snapshot!("test_origin_line_midpoint", src_delta.text.as_str());
14142 assert_eq!(
14143 scene_delta.new_graph.objects.len(),
14144 6,
14145 "{:#?}",
14146 scene_delta.new_graph.objects
14147 );
14148
14149 ctx.close().await;
14150 }
14151
14152 #[tokio::test(flavor = "multi_thread")]
14153 async fn test_origin_arc_midpoint() {
14154 let initial_source = "\
14155sketch(on = XY) {
14156 arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
14157}
14158";
14159
14160 let program = Program::parse(initial_source).unwrap().0.unwrap();
14161
14162 let mut frontend = FrontendState::new();
14163
14164 let ctx = ExecutorContext::new_mock(None).await;
14165 let version = Version(0);
14166
14167 frontend.program = program.clone();
14168 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14169 frontend.update_state_after_exec(outcome, true);
14170 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14171 let sketch_id = sketch_object.id;
14172 let sketch = expect_sketch(sketch_object);
14173 let arc_id = *sketch.segments.get(3).unwrap();
14174
14175 let constraint = Constraint::Midpoint(Midpoint {
14176 point: ConstraintSegment::ORIGIN,
14177 segment: arc_id,
14178 });
14179 let (src_delta, scene_delta) = frontend
14180 .add_constraint(&ctx, version, sketch_id, constraint)
14181 .await
14182 .unwrap();
14183 insta::assert_snapshot!("test_origin_arc_midpoint", src_delta.text.as_str());
14184 assert_eq!(
14185 scene_delta.new_graph.objects.len(),
14186 7,
14187 "{:#?}",
14188 scene_delta.new_graph.objects
14189 );
14190
14191 ctx.close().await;
14192 }
14193
14194 #[tokio::test(flavor = "multi_thread")]
14195 async fn test_segments_symmetric_arcs() {
14196 let initial_source = "\
14197sketch(on = XY) {
14198 arc(start = [var -15, var 0], end = [var -10, var 5], center = [var -10, var 0])
14199 arc(start = [var 6, var 2], end = [var 12, var -4], center = [var 8, var 1])
14200 line(start = [var 0, var -10], end = [var 0, var 10])
14201}
14202";
14203
14204 let program = Program::parse(initial_source).unwrap().0.unwrap();
14205
14206 let mut frontend = FrontendState::new();
14207
14208 let ctx = ExecutorContext::new_mock(None).await;
14209 let version = Version(0);
14210
14211 frontend.program = program.clone();
14212 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14213 frontend.update_state_after_exec(outcome, true);
14214 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14215 let sketch_id = sketch_object.id;
14216 let sketch = expect_sketch(sketch_object);
14217 let arc1_id = *sketch.segments.get(3).unwrap();
14218 let arc2_id = *sketch.segments.get(7).unwrap();
14219 let axis_id = *sketch.segments.get(10).unwrap();
14220
14221 let constraint = Constraint::Symmetric(Symmetric {
14222 input: vec![arc1_id, arc2_id],
14223 axis: axis_id,
14224 });
14225 let (src_delta, scene_delta) = frontend
14226 .add_constraint(&ctx, version, sketch_id, constraint)
14227 .await
14228 .unwrap();
14229 insta::assert_snapshot!("test_segments_symmetric_arcs", src_delta.text.as_str());
14230 assert_eq!(
14231 scene_delta.new_graph.objects.len(),
14232 14,
14233 "{:#?}",
14234 scene_delta.new_graph.objects
14235 );
14236
14237 ctx.close().await;
14238 }
14239
14240 #[tokio::test(flavor = "multi_thread")]
14241 async fn test_sketch_on_face_simple() {
14242 let initial_source = "\
14243len = 2mm
14244cube = startSketchOn(XY)
14245 |> startProfile(at = [0, 0])
14246 |> line(end = [len, 0], tag = $side)
14247 |> line(end = [0, len])
14248 |> line(end = [-len, 0])
14249 |> line(end = [0, -len])
14250 |> close()
14251 |> extrude(length = len)
14252
14253face = faceOf(cube, face = side)
14254";
14255
14256 let program = Program::parse(initial_source).unwrap().0.unwrap();
14257
14258 let mut frontend = FrontendState::new();
14259
14260 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
14261 let mock_ctx = ExecutorContext::new_mock(None).await;
14262 let version = Version(0);
14263
14264 frontend.hack_set_program(&ctx, program).await.unwrap();
14265 let face_object = find_first_face_object(&frontend.scene_graph).unwrap();
14266 let face_id = face_object.id;
14267
14268 let sketch_args = SketchCtor {
14269 on: Plane::Object(face_id),
14270 };
14271 let (_src_delta, scene_delta, sketch_id) = frontend
14272 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14273 .await
14274 .unwrap();
14275 assert_eq!(sketch_id, ObjectId(2));
14276 assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
14277 let sketch_object = &scene_delta.new_graph.objects[2];
14278 assert_eq!(sketch_object.id, ObjectId(2));
14279 assert_eq!(
14280 sketch_object.kind,
14281 ObjectKind::Sketch(Sketch {
14282 args: SketchCtor {
14283 on: Plane::Object(face_id),
14284 },
14285 plane: face_id,
14286 segments: vec![],
14287 constraints: vec![],
14288 })
14289 );
14290 assert_eq!(scene_delta.new_graph.objects.len(), 8);
14291
14292 ctx.close().await;
14293 mock_ctx.close().await;
14294 }
14295
14296 #[tokio::test(flavor = "multi_thread")]
14297 async fn test_new_sketch_on_primitive_index_face() {
14298 let initial_source = "\
14299@settings(kclVersion = 2.0)
14300
14301sketch001 = sketch(on = XY) {
14302 circle1 = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
14303}
14304extrude001 = extrude(region(point = [0mm, 0mm], sketch = sketch001), length = 5, tagEnd = $capEnd001)
14305shell001 = shell(extrude001, faces = capEnd001, thickness = 1)";
14306 let program = Program::parse(initial_source).unwrap().0.unwrap();
14307 let ctx = ExecutorContext::new_mock(None).await;
14308 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14309 let solid_id = match outcome.variables.get("shell001") {
14310 Some(KclValueView::Solid { value }) => value.id,
14311 value => panic!("expected shell001 to be a solid, got {value:?}"),
14312 };
14313 let solid_references = solid_references_from_variables(&program.ast, &outcome.variables);
14314
14315 let mut ast = program.ast;
14316 let scene_graph = SceneGraph::empty(ProjectId(0), FileId(0), Version(0));
14317 let face_expr = sketch_on_ast_expr(
14318 &mut ast,
14319 &scene_graph,
14320 &solid_references,
14321 &Plane::PrimitiveFace(kcl_api::PrimitiveFacePlane { solid_id, index: 6 }),
14322 )
14323 .unwrap();
14324 let face_decl = ast::VariableDeclaration::new(
14325 ast::VariableDeclarator::new("face001", face_expr),
14326 ast::ItemVisibility::Default,
14327 ast::VariableKind::Const,
14328 );
14329 ast.body
14330 .push(ast::BodyItem::VariableDeclaration(BoxNode::new(ast::Node::no_src(
14331 face_decl,
14332 ))));
14333 let face_source = source_from_ast(&ast);
14334 let new_source = format!("{face_source}sketch002 = sketch(on = face001) {{\n}}\n");
14335 insta::assert_snapshot!("test_new_sketch_on_primitive_index_face", new_source);
14336
14337 let program = Program::parse(&new_source).unwrap().0.unwrap();
14338 ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14339 ctx.close().await;
14340 }
14341
14342 #[tokio::test(flavor = "multi_thread")]
14343 async fn test_sketch_on_wall_artifact_from_region_extrude() {
14344 let initial_source = "\
14345s = sketch(on = YZ) {
14346 line1 = line(start = [0, 0], end = [0, 1])
14347 line2 = line(start = [0, 1], end = [1, 1])
14348 line3 = line(start = [1, 1], end = [0, 0])
14349}
14350region001 = region(point = [0.1, 0.1], sketch = s)
14351extrude001 = extrude(region001, length = 5)
14352";
14353
14354 let program = Program::parse(initial_source).unwrap().0.unwrap();
14355
14356 let mut frontend = FrontendState::new();
14357 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
14358 let version = Version(0);
14359
14360 frontend.hack_set_program(&ctx, program).await.unwrap();
14361 let wall_object_id = find_first_wall_object_id(&frontend.scene_graph).expect("expected a wall object");
14362
14363 let sketch_args = SketchCtor {
14364 on: Plane::Object(wall_object_id),
14365 };
14366 let (src_delta, _scene_delta, _sketch_id) = frontend
14367 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14368 .await
14369 .unwrap();
14370 assert!(src_delta.text.contains("faceOf(extrude001, face = region001.tags."));
14371
14372 ctx.close().await;
14373 }
14374
14375 #[tokio::test(flavor = "multi_thread")]
14376 async fn test_sketch_on_wall_artifact_from_split_region_extrude() {
14377 let initial_source = "\
14378sketch001 = sketch(on = YZ) {
14379 line1 = line(start = [var 0.49, var -0.39], end = [var 6.52, var -0.39])
14380 line2 = line(start = [var 6.52, var -0.39], end = [var 6.52, var 4.9])
14381 line3 = line(start = [var 6.52, var 4.9], end = [var 0.49, var 4.9])
14382 line4 = line(start = [var 0.49, var 4.9], end = [var 0.49, var -0.39])
14383 coincident([line1.end, line2.start])
14384 coincident([line2.end, line3.start])
14385 coincident([line3.end, line4.start])
14386 coincident([line4.end, line1.start])
14387 parallel([line2, line4])
14388 parallel([line3, line1])
14389 perpendicular([line1, line2])
14390 horizontal(line3)
14391 line5 = line(start = [2.35, 6.65], end = [5.89, -2.7])
14392}
14393region001 = region(point = [3.1, 3.74], sketch = sketch001)
14394extrude001 = extrude(region001, length = 5)
14395";
14396
14397 let program = Program::parse(initial_source).unwrap().0.unwrap();
14398
14399 let mut frontend = FrontendState::new();
14400 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
14401 let version = Version(0);
14402
14403 frontend.hack_set_program(&ctx, program).await.unwrap();
14404 let wall_object_id = find_first_wall_object_id(&frontend.scene_graph).expect("expected a wall object");
14405
14406 let sketch_args = SketchCtor {
14407 on: Plane::Object(wall_object_id),
14408 };
14409 let (src_delta, _scene_delta, _sketch_id) = frontend
14410 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14411 .await
14412 .unwrap();
14413 assert!(src_delta.text.contains("faceOf(extrude001, face = region001.tags."));
14414
14415 ctx.close().await;
14416 }
14417
14418 #[tokio::test(flavor = "multi_thread")]
14419 async fn test_new_sketch_on_multi_region_extrude_cap_indexes_selected_solid() {
14420 let initial_source = "\
14421@settings(kclVersion = 2.0)
14422
14423sketch001 = sketch(on = XY) {
14424 circle1 = circle(start = [var -1.51mm, var 1.31mm], center = [var -1.98mm, var 1.03mm])
14425 circle2 = circle(start = [var 2.98mm, var 2.37mm], center = [var 2.66mm, var 1.46mm])
14426}
14427hidden001 = hide(sketch001)
14428region001 = region(segments = [sketch001.circle2])
14429region002 = region(segments = [sketch001.circle1])
14430extrude001 = extrude([region001, region002], length = 5)
14431";
14432
14433 let program = Program::parse(initial_source).unwrap().0.unwrap();
14434 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
14435 let version = Version(0);
14436
14437 for (solid_output_index, expected_face) in [
14438 (0, "faceOf(extrude001[0], face = END)"),
14439 (1, "faceOf(extrude001[1], face = END)"),
14440 ] {
14441 let mut frontend = FrontendState::new();
14442 frontend.hack_set_program(&ctx, program.clone()).await.unwrap();
14443 let cap_object_id = find_cap_object_id_with_solid_output_index(
14444 &frontend.scene_graph,
14445 kcl_api::CapKind::End,
14446 solid_output_index,
14447 )
14448 .unwrap_or_else(|| panic!("expected an end cap object for solid output index {solid_output_index}"));
14449
14450 let sketch_args = SketchCtor {
14451 on: Plane::Object(cap_object_id),
14452 };
14453 let (src_delta, _scene_delta, _sketch_id) = frontend
14454 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14455 .await
14456 .unwrap();
14457
14458 assert!(
14459 src_delta.text.contains(expected_face),
14460 "expected `{expected_face}` in:\n{}",
14461 src_delta.text
14462 );
14463 assert!(!src_delta.text.contains("faceOf(extrude001, face = END)"));
14464 }
14465
14466 ctx.close().await;
14467 }
14468
14469 #[tokio::test(flavor = "multi_thread")]
14470 async fn test_new_sketch_on_multi_region_extrude_wall_indexes_selected_solid() {
14471 let initial_source = "\
14472@settings(kclVersion = 2.0)
14473
14474sketch001 = sketch(on = XY) {
14475 rect1Line1 = line(start = [0, 0], end = [1, 0])
14476 rect1Line2 = line(start = [1, 0], end = [1, 1])
14477 rect1Line3 = line(start = [1, 1], end = [0, 1])
14478 rect1Line4 = line(start = [0, 1], end = [0, 0])
14479 rect2Line1 = line(start = [3, 0], end = [4, 0])
14480 rect2Line2 = line(start = [4, 0], end = [4, 1])
14481 rect2Line3 = line(start = [4, 1], end = [3, 1])
14482 rect2Line4 = line(start = [3, 1], end = [3, 0])
14483}
14484hidden001 = hide(sketch001)
14485region001 = region(segments = [
14486 sketch001.rect1Line4,
14487 sketch001.rect1Line1
14488])
14489region002 = region(segments = [
14490 sketch001.rect2Line4,
14491 sketch001.rect2Line1
14492])
14493extrude001 = extrude([region001, region002], length = 5)
14494";
14495
14496 let program = Program::parse(initial_source).unwrap().0.unwrap();
14497 let mut frontend = FrontendState::new();
14498 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
14499 let version = Version(0);
14500
14501 frontend.hack_set_program(&ctx, program).await.unwrap();
14502 let region_call = "\
14503region(segments = [
14504 sketch001.rect1Line4,
14505 sketch001.rect1Line1
14506])";
14507 let region_call_start = initial_source.find(region_call).unwrap();
14508 let region_range = [region_call_start, region_call_start + region_call.len(), 0].into();
14509 let segment_call = "line(start = [0, 0], end = [1, 0])";
14510 let segment_call_start = initial_source.find(segment_call).unwrap();
14511 let segment_range = [segment_call_start, segment_call_start + segment_call.len(), 0].into();
14512 let wall_object_id = frontend
14513 .scene_graph
14514 .objects
14515 .iter()
14516 .find_map(|object| match &object.kind {
14517 ObjectKind::Wall(wall)
14518 if wall.source.path.as_ref().is_some_and(|path| path.range == region_range)
14519 && wall.source.segment.range == segment_range =>
14520 {
14521 Some(object.id)
14522 }
14523 _ => None,
14524 })
14525 .expect("expected a wall object for region001.tags.rect1Line1");
14526
14527 let sketch_args = SketchCtor {
14528 on: Plane::Object(wall_object_id),
14529 };
14530 let (src_delta, _scene_delta, _sketch_id) = frontend
14531 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14532 .await
14533 .unwrap();
14534
14535 let expected_face = "faceOf(extrude001[0], face = region001.tags.rect1Line1)";
14536 assert!(
14537 src_delta.text.contains(expected_face),
14538 "expected `{expected_face}` in:\n{}",
14539 src_delta.text
14540 );
14541 assert!(!src_delta.text.contains("faceOf(extrude001, face ="));
14542
14543 ctx.close().await;
14544 }
14545
14546 #[test]
14547 fn test_enclosing_variable_fallback_skips_nested_sketch_items() {
14548 let source = "\
14549sketch001 = sketch(on = XY) {
14550 line(start = [0, 0], end = [1, 0])
14551}
14552part = subtract(boxSolid, tools = [cutSolid])
14553 |> appearance(color = \"#8f96a3\")
14554";
14555 let ast = Program::parse(source).unwrap().0.unwrap().ast;
14556 let line_start = source.find("line").unwrap();
14557 let line_end = line_start + "line(start = [0, 0], end = [1, 0])".len();
14558 let line_ref = SourceRef::Simple {
14559 range: [line_start, line_end, 0].into(),
14560 node_path: None,
14561 };
14562 assert_eq!(variable_name_containing_source_ref(&ast, &line_ref), None);
14563
14564 let subtract_start = source.find("subtract").unwrap();
14565 let subtract_end = subtract_start + "subtract(boxSolid, tools = [cutSolid])".len();
14566 let subtract_ref = SourceRef::Simple {
14567 range: [subtract_start, subtract_end, 0].into(),
14568 node_path: None,
14569 };
14570 assert_eq!(
14571 variable_name_containing_source_ref(&ast, &subtract_ref),
14572 Some("part".to_owned())
14573 );
14574 }
14575
14576 #[tokio::test(flavor = "multi_thread")]
14577 async fn test_sketch_on_subtracted_sweep_cap_uses_composite_solid() {
14578 clear_mem_cache().await;
14579 let source = "\
14580boxSolid = startSketchOn(XY)
14581 |> startProfile(at = [0, 0])
14582 |> line(end = [4, 0], tag = $bottomEdge)
14583 |> line(end = [0, 4])
14584 |> line(end = [-4, 0])
14585 |> close()
14586 |> extrude(length = 10)
14587cutSolid = startSketchOn(XY)
14588 |> startProfile(at = [1, 1])
14589 |> line(end = [1, 0])
14590 |> line(end = [0, 1])
14591 |> line(end = [-1, 0])
14592 |> close()
14593 |> extrude(length = 10)
14594part = subtract(boxSolid, tools = [cutSolid])
14595 |> appearance(color = \"#8f96a3\", roughness = 55, metalness = 8)
14596";
14597 let program = Program::parse(source).unwrap().0.unwrap();
14598 let mut frontend = FrontendState::new();
14599 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
14600 match frontend.hack_set_program(&ctx, program).await.unwrap() {
14601 SetProgramOutcome::Success { .. } => {}
14602 SetProgramOutcome::ExecFailure { error } => panic!("KCL fixture failed to execute: {error:?}"),
14603 }
14604
14605 let sweep_call_start = source.find("extrude").unwrap();
14606 let sweep_call_end = sweep_call_start + "extrude(length = 10)".len();
14607 let part_call_start = source.find("subtract").unwrap();
14608 let part_call_end = part_call_start + "subtract(boxSolid, tools = [cutSolid])".len();
14609 let sweep_range = [sweep_call_start, sweep_call_end, 0].into();
14610 let composite_range = [part_call_start, part_call_end, 0].into();
14611
14612 let cap_object = frontend
14613 .scene_graph
14614 .objects
14615 .iter()
14616 .find(|object| {
14617 matches!(
14618 &object.kind,
14619 ObjectKind::Cap(kcl_api::Cap {
14620 kind: kcl_api::CapKind::End,
14621 source,
14622 ..
14623 }) if source.solid.range == composite_range && source.sweep.range == sweep_range
14624 )
14625 })
14626 .expect("expected end cap object to trace through subtract and original extrude");
14627
14628 let mut ast = frontend.program.ast.clone();
14629 let cap_expr = sketch_on_ast_expr(
14630 &mut ast,
14631 &frontend.scene_graph,
14632 &frontend.solid_references,
14633 &Plane::Object(cap_object.id),
14634 )
14635 .unwrap();
14636 let cap_face_decl = ast::VariableDeclaration::new(
14637 ast::VariableDeclarator::new("capFace", cap_expr.clone()),
14638 ast::ItemVisibility::Default,
14639 ast::VariableKind::Const,
14640 );
14641 ast.body
14642 .push(ast::BodyItem::VariableDeclaration(BoxNode::new(ast::Node::no_src(
14643 cap_face_decl,
14644 ))));
14645 let generated_source = source_from_ast(&ast);
14646
14647 assert!(generated_source.contains("capFace = faceOf(part, face = END)"));
14648 assert!(!generated_source.contains("faceOf(boxSolid"));
14649 let ast::Expr::CallExpressionKw(call) = cap_expr else {
14650 panic!("expected faceOf call");
14651 };
14652 assert_eq!(call.callee.name.name, "faceOf");
14653 let ast::Expr::Name(solid_name) = call.unlabeled.as_ref().unwrap() else {
14654 panic!("expected solid name");
14655 };
14656 assert_eq!(solid_name.name.name, "part");
14657 let ast::Expr::Name(face_name) = &call.arguments[0].arg else {
14658 panic!("expected face name");
14659 };
14660 assert_eq!(face_name.name.name, "END");
14661
14662 ctx.close().await;
14663 }
14664
14665 #[tokio::test(flavor = "multi_thread")]
14666 async fn test_sketch_on_plane_incremental() {
14667 let initial_source = "\
14668len = 2mm
14669cube = startSketchOn(XY)
14670 |> startProfile(at = [0, 0])
14671 |> line(end = [len, 0], tag = $side)
14672 |> line(end = [0, len])
14673 |> line(end = [-len, 0])
14674 |> line(end = [0, -len])
14675 |> close()
14676 |> extrude(length = len)
14677
14678plane = planeOf(cube, face = side)
14679";
14680
14681 let program = Program::parse(initial_source).unwrap().0.unwrap();
14682
14683 let mut frontend = FrontendState::new();
14684
14685 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
14686 let mock_ctx = ExecutorContext::new_mock(None).await;
14687 let version = Version(0);
14688
14689 frontend.hack_set_program(&ctx, program).await.unwrap();
14690 let plane_object = frontend
14692 .scene_graph
14693 .objects
14694 .iter()
14695 .rev()
14696 .find(|object| matches!(&object.kind, ObjectKind::Plane(_)))
14697 .unwrap();
14698 let plane_id = plane_object.id;
14699
14700 let sketch_args = SketchCtor {
14701 on: Plane::Object(plane_id),
14702 };
14703 let (src_delta, scene_delta, sketch_id) = frontend
14704 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14705 .await
14706 .unwrap();
14707 insta::assert_snapshot!("test_sketch_on_plane_incremental", src_delta.text.as_str());
14708 assert_eq!(sketch_id, ObjectId(2));
14709 assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
14710 let sketch_object = &scene_delta.new_graph.objects[2];
14711 assert_eq!(sketch_object.id, ObjectId(2));
14712 assert_eq!(
14713 sketch_object.kind,
14714 ObjectKind::Sketch(Sketch {
14715 args: SketchCtor {
14716 on: Plane::Object(plane_id),
14717 },
14718 plane: plane_id,
14719 segments: vec![],
14720 constraints: vec![],
14721 })
14722 );
14723 assert_eq!(scene_delta.new_graph.objects.len(), 9);
14724
14725 let plane_object = scene_delta.new_graph.objects.get(plane_id.0).unwrap();
14726 assert_eq!(plane_object.id, plane_id);
14727 assert_eq!(plane_object.kind, ObjectKind::Plane(Plane::Object(plane_id)));
14728
14729 ctx.close().await;
14730 mock_ctx.close().await;
14731 }
14732
14733 #[tokio::test(flavor = "multi_thread")]
14734 async fn test_new_sketch_uses_unique_variable_name() {
14735 let initial_source = "\
14736sketch1 = sketch(on = XY) {
14737}
14738";
14739
14740 let program = Program::parse(initial_source).unwrap().0.unwrap();
14741
14742 let mut frontend = FrontendState::new();
14743 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
14744 let version = Version(0);
14745
14746 frontend.hack_set_program(&ctx, program).await.unwrap();
14747
14748 let sketch_args = SketchCtor {
14749 on: Plane::Default(PlaneName::Yz),
14750 };
14751 let (src_delta, _, _) = frontend
14752 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14753 .await
14754 .unwrap();
14755
14756 insta::assert_snapshot!("test_new_sketch_uses_unique_variable_name", src_delta.text.as_str());
14757
14758 ctx.close().await;
14759 }
14760
14761 #[tokio::test(flavor = "multi_thread")]
14762 async fn test_new_sketch_twice_using_same_plane() {
14763 let initial_source = "\
14764sketch1 = sketch(on = XY) {
14765}
14766";
14767
14768 let program = Program::parse(initial_source).unwrap().0.unwrap();
14769
14770 let mut frontend = FrontendState::new();
14771 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
14772 let version = Version(0);
14773
14774 frontend.hack_set_program(&ctx, program).await.unwrap();
14775
14776 let sketch_args = SketchCtor {
14777 on: Plane::Default(PlaneName::Xy),
14778 };
14779 let (src_delta, _, _) = frontend
14780 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14781 .await
14782 .unwrap();
14783
14784 insta::assert_snapshot!("test_new_sketch_twice_using_same_plane", src_delta.text.as_str());
14785
14786 ctx.close().await;
14787 }
14788
14789 #[tokio::test(flavor = "multi_thread")]
14790 async fn test_sketch_mode_reuses_cached_on_expression() {
14791 let initial_source = "\
14792width = 2mm
14793sketch(on = offsetPlane(XY, offset = width)) {
14794 line1 = line(start = [var 0, var 0], end = [var 1mm, var 0])
14795 distance([line1.start, line1.end]) == width
14796}
14797";
14798 let program = Program::parse(initial_source).unwrap().0.unwrap();
14799
14800 let mut frontend = FrontendState::new();
14801 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
14802 let mock_ctx = ExecutorContext::new_mock(None).await;
14803 let version = Version(0);
14804 let project_id = ProjectId(0);
14805 let file_id = FileId(0);
14806
14807 frontend.hack_set_program(&ctx, program).await.unwrap();
14808 let initial_object_count = frontend.scene_graph.objects.len();
14809 let sketch_id = find_first_sketch_object(&frontend.scene_graph)
14810 .expect("Expected sketch object to exist")
14811 .id;
14812
14813 let scene_delta = frontend
14816 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
14817 .await
14818 .unwrap();
14819 assert_eq!(scene_delta.new_graph.objects.len(), initial_object_count);
14820
14821 let (_src_delta, scene_delta) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
14824 assert_eq!(scene_delta.new_graph.objects.len(), initial_object_count);
14825
14826 ctx.close().await;
14827 mock_ctx.close().await;
14828 }
14829
14830 #[tokio::test(flavor = "multi_thread")]
14831 async fn test_edit_sketch_nested_in_pipe() {
14832 clear_mem_cache().await;
14833 let source = r#"
14834profile = sketch(on = XY) {
14835 line1 = line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])
14836}
14837 |> translate(x = 2mm)
14838"#;
14839 let program = Program::parse_no_errs(source).unwrap();
14840 let mut frontend = FrontendState::new();
14841 let mock_ctx = ExecutorContext::new_mock(None).await;
14842 let version = Version(0);
14843
14844 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
14845 let sketch_id = find_first_sketch_object(&frontend.scene_graph)
14846 .expect("Expected piped sketch object")
14847 .id;
14848
14849 let scene_delta = frontend
14850 .edit_sketch(&mock_ctx, ProjectId(0), FileId(0), version, sketch_id)
14851 .await
14852 .unwrap();
14853 assert_eq!(scene_delta.new_graph.sketch_mode, Some(sketch_id));
14854 assert!(
14855 scene_delta
14856 .new_graph
14857 .objects
14858 .iter()
14859 .any(|object| matches!(&object.kind, ObjectKind::Segment { .. })),
14860 "Expected the piped sketch's segments to be present in sketch mode"
14861 );
14862
14863 clear_mem_cache().await;
14864 mock_ctx.close().await;
14865 }
14866
14867 #[tokio::test(flavor = "multi_thread")]
14868 async fn test_issue_9409_edit_sketch_nested_in_if_with_var_feedback() {
14869 clear_mem_cache().await;
14870 let source = r#"
14871useFirstProfile = true
14872
14873profile = if useFirstProfile {
14874 sketch(on = XY) {
14875 line1 = line(start = [0mm, 0mm], end = [var 20mm, var 10mm])
14876 }
14877} else {
14878 sketch(on = XY) {
14879 line2 = line(start = [0mm, 0mm], end = [var 10mm, var 20mm])
14880 }
14881}
14882"#;
14883 let program = Program::parse_no_errs(source).unwrap();
14884 let mut frontend = FrontendState::new();
14885 let mock_ctx = ExecutorContext::new_mock(None).await;
14886 let version = Version(0);
14887
14888 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
14889 let sketch_object =
14890 find_first_sketch_object(&frontend.scene_graph).expect("Expected active branch's sketch object");
14891 let sketch_id = sketch_object.id;
14892 let sketch = expect_sketch(sketch_object);
14893 let line_end_id = *sketch
14894 .segments
14895 .get(1)
14896 .expect("Expected the active branch's line end point");
14897
14898 let scene_delta = frontend
14899 .edit_sketch(&mock_ctx, ProjectId(0), FileId(0), version, sketch_id)
14900 .await
14901 .unwrap();
14902 assert_eq!(scene_delta.new_graph.sketch_mode, Some(sketch_id));
14903
14904 let segments = vec![ExistingSegmentCtor {
14905 id: line_end_id,
14906 ctor: SegmentCtor::Point(PointCtor {
14907 position: Point2d {
14908 x: Expr::Var(Number {
14909 value: 30.0,
14910 units: NumericSuffix::Mm,
14911 }),
14912 y: Expr::Var(Number {
14913 value: 15.0,
14914 units: NumericSuffix::Mm,
14915 }),
14916 },
14917 }),
14918 }];
14919 let (source_delta, _) = frontend
14920 .edit_segments(&mock_ctx, version, sketch_id, segments)
14921 .await
14922 .unwrap();
14923 assert!(
14924 source_delta
14925 .text
14926 .contains("line1 = line(start = [0mm, 0mm], end = [var 30mm, var 15mm])"),
14927 "Expected the active branch's dragged variables to be updated:\n{}",
14928 source_delta.text
14929 );
14930 assert!(
14931 source_delta
14932 .text
14933 .contains("line2 = line(start = [0mm, 0mm], end = [var 10mm, var 20mm])"),
14934 "Expected the inactive branch to remain unchanged:\n{}",
14935 source_delta.text
14936 );
14937
14938 clear_mem_cache().await;
14939 mock_ctx.close().await;
14940 }
14941
14942 #[tokio::test(flavor = "multi_thread")]
14943 async fn test_multiple_sketch_blocks() {
14944 let initial_source = "\
14945// Cube that requires the engine.
14946width = 2
14947sketch001 = startSketchOn(XY)
14948profile001 = startProfile(sketch001, at = [0, 0])
14949 |> yLine(length = width, tag = $seg1)
14950 |> xLine(length = width)
14951 |> yLine(length = -width)
14952 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
14953 |> close()
14954extrude001 = extrude(profile001, length = width)
14955
14956// Get a value that requires the engine.
14957x = segLen(seg1)
14958
14959// Triangle with side length 2*x.
14960sketch(on = XY) {
14961 line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
14962 line2 = line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
14963 coincident([line1.end, line2.start])
14964 line3 = line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
14965 coincident([line2.end, line3.start])
14966 coincident([line3.end, line1.start])
14967 equalLength([line3, line1])
14968 equalLength([line1, line2])
14969 distance([line1.start, line1.end]) == 2*x
14970}
14971
14972// Line segment with length x.
14973sketch2 = sketch(on = XY) {
14974 line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
14975 distance([line1.start, line1.end]) == x
14976}
14977";
14978
14979 let program = Program::parse(initial_source).unwrap().0.unwrap();
14980
14981 let mut frontend = FrontendState::new();
14982
14983 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
14984 let mock_ctx = ExecutorContext::new_mock(None).await;
14985 let version = Version(0);
14986 let project_id = ProjectId(0);
14987 let file_id = FileId(0);
14988
14989 frontend.hack_set_program(&ctx, program).await.unwrap();
14990 let sketch_objects = frontend
14991 .scene_graph
14992 .objects
14993 .iter()
14994 .filter(|obj| matches!(obj.kind, ObjectKind::Sketch(_)))
14995 .collect::<Vec<_>>();
14996 let sketch1_id = sketch_objects.first().unwrap().id;
14997 let sketch2_id = sketch_objects.get(1).unwrap().id;
14998 let point1_id = ObjectId(sketch1_id.0 + 1);
15000 let point2_id = ObjectId(sketch2_id.0 + 1);
15002
15003 let scene_delta = frontend
15012 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch1_id)
15013 .await
15014 .unwrap();
15015 assert_eq!(
15016 scene_delta.new_graph.objects.len(),
15017 18,
15018 "{:#?}",
15019 scene_delta.new_graph.objects
15020 );
15021
15022 let point_ctor = PointCtor {
15024 position: Point2d {
15025 x: Expr::Var(Number {
15026 value: 1.0,
15027 units: NumericSuffix::Mm,
15028 }),
15029 y: Expr::Var(Number {
15030 value: 2.0,
15031 units: NumericSuffix::Mm,
15032 }),
15033 },
15034 };
15035 let segments = vec![ExistingSegmentCtor {
15036 id: point1_id,
15037 ctor: SegmentCtor::Point(point_ctor),
15038 }];
15039 let (src_delta, _) = frontend
15040 .edit_segments(&mock_ctx, version, sketch1_id, segments)
15041 .await
15042 .unwrap();
15043 insta::assert_snapshot!("test_multiple_sketch_blocks_1", src_delta.text.as_str());
15045 let edited_sketch1_source = src_delta.text.clone();
15046
15047 let (src_delta, _) = frontend.execute_mock(&mock_ctx, version, sketch1_id).await.unwrap();
15049 assert_eq!(src_delta.text, edited_sketch1_source);
15050 let scene = frontend.exit_sketch(&ctx, version, sketch1_id).await.unwrap();
15058 assert_eq!(scene.objects.len(), 30, "{:#?}", scene.objects);
15059
15060 let scene_delta = frontend
15068 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch2_id)
15069 .await
15070 .unwrap();
15071 assert_eq!(
15072 scene_delta.new_graph.objects.len(),
15073 24,
15074 "{:#?}",
15075 scene_delta.new_graph.objects
15076 );
15077
15078 let point_ctor = PointCtor {
15080 position: Point2d {
15081 x: Expr::Var(Number {
15082 value: 3.0,
15083 units: NumericSuffix::Mm,
15084 }),
15085 y: Expr::Var(Number {
15086 value: 4.0,
15087 units: NumericSuffix::Mm,
15088 }),
15089 },
15090 };
15091 let segments = vec![ExistingSegmentCtor {
15092 id: point2_id,
15093 ctor: SegmentCtor::Point(point_ctor),
15094 }];
15095 let (src_delta, _) = frontend
15096 .edit_segments(&mock_ctx, version, sketch2_id, segments)
15097 .await
15098 .unwrap();
15099 insta::assert_snapshot!("test_multiple_sketch_blocks_2", src_delta.text.as_str());
15101 let edited_sketch2_source = src_delta.text.clone();
15102
15103 let (src_delta, _) = frontend.execute_mock(&mock_ctx, version, sketch2_id).await.unwrap();
15105 assert_eq!(src_delta.text, edited_sketch2_source);
15106
15107 ctx.close().await;
15108 mock_ctx.close().await;
15109 }
15110
15111 #[tokio::test(flavor = "multi_thread")]
15112 async fn test_exit_sketch_without_changes_allows_entering_next_sketch() {
15113 clear_mem_cache().await;
15114
15115 let source = r#"sketch001 = sketch(on = XZ) {
15116 circle1 = circle(start = [var -1.96mm, var 2.77mm], center = [var -2.69mm, var 3.44mm])
15117}
15118sketch002 = sketch(on = XY) {
15119 line1 = line(start = [var 0mm, var 0mm], end = [var 4.68mm, var 0mm])
15120 line2 = line(start = [var 4.68mm, var 0mm], end = [var 4.68mm, var 2.96mm])
15121 line3 = line(start = [var 4.68mm, var 2.96mm], end = [var 0mm, var 2.96mm])
15122 line4 = line(start = [var 0mm, var 2.96mm], end = [var 0mm, var 0mm])
15123 coincident([line1.end, line2.start])
15124 coincident([line2.end, line3.start])
15125 coincident([line3.end, line4.start])
15126 coincident([line4.end, line1.start])
15127 parallel([line2, line4])
15128 parallel([line3, line1])
15129 perpendicular([line1, line2])
15130 horizontal(line3)
15131 coincident([line1.start, ORIGIN])
15132}
15133"#;
15134
15135 let program = Program::parse(source).unwrap().0.unwrap();
15136 let mut frontend = FrontendState::new();
15137 let ctx = ExecutorContext::new_with_engine(sync::Arc::new(EngineManager::new_mock()), Default::default());
15138 let mock_ctx = ExecutorContext::new_mock(None).await;
15139 let version = Version(0);
15140 let project_id = ProjectId(0);
15141 let file_id = FileId(0);
15142
15143 frontend.hack_set_program(&ctx, program).await.unwrap();
15144 let sketch_objects = frontend
15145 .scene_graph
15146 .objects
15147 .iter()
15148 .filter(|object| matches!(object.kind, ObjectKind::Sketch(_)))
15149 .collect::<Vec<_>>();
15150 assert_eq!(sketch_objects.len(), 2, "{:#?}", frontend.scene_graph.objects);
15151
15152 let sketch1_id = sketch_objects[0].id;
15153 let sketch2_id = sketch_objects[1].id;
15154
15155 frontend
15156 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch1_id)
15157 .await
15158 .unwrap();
15159 frontend.exit_sketch(&ctx, version, sketch1_id).await.unwrap();
15160
15161 let scene_delta = frontend
15162 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch2_id)
15163 .await
15164 .unwrap();
15165 assert_eq!(scene_delta.new_graph.sketch_mode, Some(sketch2_id));
15166
15167 clear_mem_cache().await;
15168 ctx.close().await;
15169 mock_ctx.close().await;
15170 }
15171
15172 #[tokio::test(flavor = "multi_thread")]
15177 async fn test_extra_newlines_after_settings_edit_sketch_add_point() {
15178 let initial_source = "@settings(defaultLengthUnit = mm)
15180
15181sketch001 = sketch(on = XY) {
15182 point(at = [1in, 2in])
15183}
15184";
15185
15186 let program = Program::parse(initial_source).unwrap().0.unwrap();
15187 let mut frontend = FrontendState::new();
15188
15189 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
15190 let mock_ctx = ExecutorContext::new_mock(None).await;
15191 let version = Version(0);
15192 let project_id = ProjectId(0);
15193 let file_id = FileId(0);
15194
15195 frontend.hack_set_program(&ctx, program).await.unwrap();
15196 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15197 let sketch_id = sketch_object.id;
15198
15199 frontend
15201 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
15202 .await
15203 .unwrap();
15204
15205 let point_ctor = PointCtor {
15207 position: Point2d {
15208 x: Expr::Number(Number {
15209 value: 5.0,
15210 units: NumericSuffix::Mm,
15211 }),
15212 y: Expr::Number(Number {
15213 value: 6.0,
15214 units: NumericSuffix::Mm,
15215 }),
15216 },
15217 };
15218 let segment = SegmentCtor::Point(point_ctor);
15219 let (src_delta, scene_delta) = frontend
15220 .add_segment(&mock_ctx, version, sketch_id, segment, None)
15221 .await
15222 .unwrap();
15223 assert!(
15225 src_delta.text.contains("point(at = [5mm, 6mm])"),
15226 "Expected new point in source, got: {}",
15227 src_delta.text
15228 );
15229 assert!(!scene_delta.new_objects.is_empty());
15230
15231 ctx.close().await;
15232 mock_ctx.close().await;
15233 }
15234
15235 #[tokio::test(flavor = "multi_thread")]
15236 async fn test_ensure_control_point_spline_experimental_features_adds_allow_setting() {
15237 let initial_program = Program::parse("s = sketch(on = XY) {}\n").unwrap().0.unwrap();
15238
15239 let updated_program = ensure_control_point_spline_experimental_features(&initial_program).unwrap();
15240 let meta_settings = updated_program.meta_settings().unwrap().unwrap();
15241
15242 assert_eq!(meta_settings.experimental_features, WarningLevel::Allow);
15243 assert!(
15244 source_from_ast(&updated_program.ast).contains("@settings(experimentalFeatures = allow)"),
15245 "Expected experimental settings to be added to source"
15246 );
15247 }
15248
15249 #[tokio::test(flavor = "multi_thread")]
15250 async fn test_extra_newlines_after_settings_add_line_to_empty_sketch() {
15251 let initial_source = "@settings(defaultLengthUnit = mm)
15253
15254s = sketch(on = XY) {}
15255";
15256
15257 let program = Program::parse(initial_source).unwrap().0.unwrap();
15258 let mut frontend = FrontendState::new();
15259
15260 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
15261 let mock_ctx = ExecutorContext::new_mock(None).await;
15262 let version = Version(0);
15263
15264 frontend.hack_set_program(&ctx, program).await.unwrap();
15265 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15266 let sketch_id = sketch_object.id;
15267
15268 let line_ctor = LineCtor {
15269 start: Point2d {
15270 x: Expr::Number(Number {
15271 value: 0.0,
15272 units: NumericSuffix::Mm,
15273 }),
15274 y: Expr::Number(Number {
15275 value: 0.0,
15276 units: NumericSuffix::Mm,
15277 }),
15278 },
15279 end: Point2d {
15280 x: Expr::Number(Number {
15281 value: 10.0,
15282 units: NumericSuffix::Mm,
15283 }),
15284 y: Expr::Number(Number {
15285 value: 10.0,
15286 units: NumericSuffix::Mm,
15287 }),
15288 },
15289 construction: None,
15290 };
15291 let segment = SegmentCtor::Line(line_ctor);
15292 let (src_delta, scene_delta) = frontend
15293 .add_segment(&mock_ctx, version, sketch_id, segment, None)
15294 .await
15295 .unwrap();
15296 assert!(
15297 src_delta.text.contains("line(start = [0mm, 0mm], end = [10mm, 10mm])"),
15298 "Expected line in source, got: {}",
15299 src_delta.text
15300 );
15301 assert_eq!(scene_delta.new_objects.len(), 3);
15303
15304 ctx.close().await;
15305 mock_ctx.close().await;
15306 }
15307
15308 #[tokio::test(flavor = "multi_thread")]
15309 async fn test_extra_newlines_between_operations_edit_line() {
15310 let initial_source = "@settings(defaultLengthUnit = mm)
15312
15313sketch001 = sketch(on = XY) {
15314
15315 line1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm])
15316
15317}
15318";
15319
15320 let program = Program::parse(initial_source).unwrap().0.unwrap();
15321 let mut frontend = FrontendState::new();
15322
15323 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
15324 let mock_ctx = ExecutorContext::new_mock(None).await;
15325 let version = Version(0);
15326 let project_id = ProjectId(0);
15327 let file_id = FileId(0);
15328
15329 let outcome = frontend.hack_set_program(&ctx, program).await.unwrap();
15330 assert!(matches!(outcome, SetProgramOutcome::Success { .. }), "{outcome:?}");
15331 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15332 let sketch_id = sketch_object.id;
15333 let sketch = expect_sketch(sketch_object);
15334
15335 let line_id = sketch
15337 .segments
15338 .iter()
15339 .copied()
15340 .find(|seg_id| {
15341 matches!(
15342 &frontend.scene_graph.objects[seg_id.0].kind,
15343 ObjectKind::Segment {
15344 segment: Segment::Line(_)
15345 }
15346 )
15347 })
15348 .expect("Expected a line segment in sketch");
15349
15350 frontend
15352 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
15353 .await
15354 .unwrap();
15355
15356 let line_ctor = LineCtor {
15358 start: Point2d {
15359 x: Expr::Var(Number {
15360 value: 1.0,
15361 units: NumericSuffix::Mm,
15362 }),
15363 y: Expr::Var(Number {
15364 value: 2.0,
15365 units: NumericSuffix::Mm,
15366 }),
15367 },
15368 end: Point2d {
15369 x: Expr::Var(Number {
15370 value: 13.0,
15371 units: NumericSuffix::Mm,
15372 }),
15373 y: Expr::Var(Number {
15374 value: 14.0,
15375 units: NumericSuffix::Mm,
15376 }),
15377 },
15378 construction: None,
15379 };
15380 let segments = vec![ExistingSegmentCtor {
15381 id: line_id,
15382 ctor: SegmentCtor::Line(line_ctor),
15383 }];
15384 let (src_delta, _scene_delta) = frontend
15385 .edit_segments(&mock_ctx, version, sketch_id, segments)
15386 .await
15387 .unwrap();
15388 assert!(
15389 src_delta
15390 .text
15391 .contains("line(start = [var 1mm, var 2mm], end = [var 13mm, var 14mm])"),
15392 "Expected edited line in source, got: {}",
15393 src_delta.text
15394 );
15395
15396 ctx.close().await;
15397 mock_ctx.close().await;
15398 }
15399
15400 #[tokio::test(flavor = "multi_thread")]
15401 async fn test_extra_newlines_delete_segment() {
15402 let initial_source = "@settings(defaultLengthUnit = mm)
15404
15405sketch001 = sketch(on = XY) {
15406 circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
15407}
15408";
15409
15410 let program = Program::parse(initial_source).unwrap().0.unwrap();
15411 let mut frontend = FrontendState::new();
15412
15413 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
15414 let mock_ctx = ExecutorContext::new_mock(None).await;
15415 let version = Version(0);
15416
15417 frontend.hack_set_program(&ctx, program).await.unwrap();
15418 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15419 let sketch_id = sketch_object.id;
15420 let sketch = expect_sketch(sketch_object);
15421
15422 assert_eq!(sketch.segments.len(), 3);
15424 let circle_id = sketch.segments[2];
15425
15426 let (src_delta, scene_delta) = frontend
15428 .delete_objects(&mock_ctx, version, sketch_id, vec![], vec![circle_id])
15429 .await
15430 .unwrap();
15431 assert!(
15432 src_delta.text.contains("sketch(on = XY) {"),
15433 "Expected sketch block in source, got: {}",
15434 src_delta.text
15435 );
15436 let new_sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
15437 let new_sketch = expect_sketch(new_sketch_object);
15438 assert_eq!(new_sketch.segments.len(), 0);
15439
15440 ctx.close().await;
15441 mock_ctx.close().await;
15442 }
15443
15444 #[tokio::test(flavor = "multi_thread")]
15445 async fn test_unformatted_source_add_arc() {
15446 let initial_source = "@settings(defaultLengthUnit = mm)
15448
15449sketch001 = sketch(on = XY) {
15450}
15451";
15452
15453 let program = Program::parse(initial_source).unwrap().0.unwrap();
15454 let mut frontend = FrontendState::new();
15455
15456 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
15457 let mock_ctx = ExecutorContext::new_mock(None).await;
15458 let version = Version(0);
15459
15460 frontend.hack_set_program(&ctx, program).await.unwrap();
15461 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15462 let sketch_id = sketch_object.id;
15463
15464 let arc_ctor = ArcCtor {
15465 start: Point2d {
15466 x: Expr::Var(Number {
15467 value: 5.0,
15468 units: NumericSuffix::Mm,
15469 }),
15470 y: Expr::Var(Number {
15471 value: 0.0,
15472 units: NumericSuffix::Mm,
15473 }),
15474 },
15475 end: Point2d {
15476 x: Expr::Var(Number {
15477 value: 0.0,
15478 units: NumericSuffix::Mm,
15479 }),
15480 y: Expr::Var(Number {
15481 value: 5.0,
15482 units: NumericSuffix::Mm,
15483 }),
15484 },
15485 center: Point2d {
15486 x: Expr::Var(Number {
15487 value: 0.0,
15488 units: NumericSuffix::Mm,
15489 }),
15490 y: Expr::Var(Number {
15491 value: 0.0,
15492 units: NumericSuffix::Mm,
15493 }),
15494 },
15495 direction: None,
15496 construction: None,
15497 };
15498 let segment = SegmentCtor::Arc(arc_ctor);
15499 let (src_delta, scene_delta) = frontend
15500 .add_segment(&mock_ctx, version, sketch_id, segment, None)
15501 .await
15502 .unwrap();
15503 assert!(
15504 src_delta
15505 .text
15506 .contains("arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])"),
15507 "Expected arc in source, got: {}",
15508 src_delta.text
15509 );
15510 assert!(!scene_delta.new_objects.is_empty());
15511
15512 ctx.close().await;
15513 mock_ctx.close().await;
15514 }
15515
15516 #[tokio::test(flavor = "multi_thread")]
15517 async fn test_arc_direction_flows_to_source() {
15518 let initial_source = "@settings(defaultLengthUnit = mm)
15519
15520sketch001 = sketch(on = XY) {
15521}
15522";
15523
15524 let program = Program::parse(initial_source).unwrap().0.unwrap();
15525 let mut frontend = FrontendState::new();
15526
15527 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
15528 let mock_ctx = ExecutorContext::new_mock(None).await;
15529 let version = Version(0);
15530
15531 frontend.hack_set_program(&ctx, program).await.unwrap();
15532 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15533 let sketch_id = sketch_object.id;
15534
15535 let point = |x: f64, y: f64| Point2d {
15536 x: Expr::Var(Number {
15537 value: x,
15538 units: NumericSuffix::Mm,
15539 }),
15540 y: Expr::Var(Number {
15541 value: y,
15542 units: NumericSuffix::Mm,
15543 }),
15544 };
15545
15546 let arc_ctor = ArcCtor {
15548 start: point(5.0, 0.0),
15549 end: point(0.0, 5.0),
15550 center: point(0.0, 0.0),
15551 direction: Some(ArcDirection::Cw),
15552 construction: None,
15553 };
15554 let (src_delta, scene_delta) = frontend
15555 .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Arc(arc_ctor), None)
15556 .await
15557 .unwrap();
15558 assert!(
15559 src_delta.text.contains("direction = CW"),
15560 "Expected direction = CW in source, got: {}",
15561 src_delta.text
15562 );
15563 let arc_id = *scene_delta.new_objects.last().unwrap();
15565
15566 let edited_ctor = ArcCtor {
15570 start: point(0.0, -5.0),
15571 end: point(0.0, 5.0),
15572 center: point(0.0, 0.0),
15573 direction: Some(ArcDirection::Cw),
15574 construction: None,
15575 };
15576 let (src_delta, _scene_delta) = frontend
15577 .edit_segments(
15578 &mock_ctx,
15579 version,
15580 sketch_id,
15581 vec![ExistingSegmentCtor {
15582 id: arc_id,
15583 ctor: SegmentCtor::Arc(edited_ctor),
15584 }],
15585 )
15586 .await
15587 .unwrap();
15588 assert!(
15589 src_delta.text.contains("start = [var 0mm, var -5mm]"),
15590 "Expected edited start point in source, got: {}",
15591 src_delta.text
15592 );
15593 assert!(
15594 src_delta.text.contains("direction = CW"),
15595 "Expected direction = CW to be preserved in source, got: {}",
15596 src_delta.text
15597 );
15598
15599 let edited_ctor = ArcCtor {
15602 start: point(0.0, -5.0),
15603 end: point(0.0, 5.0),
15604 center: point(0.0, 0.0),
15605 direction: Some(ArcDirection::Ccw),
15606 construction: None,
15607 };
15608 let (src_delta, _scene_delta) = frontend
15609 .edit_segments(
15610 &mock_ctx,
15611 version,
15612 sketch_id,
15613 vec![ExistingSegmentCtor {
15614 id: arc_id,
15615 ctor: SegmentCtor::Arc(edited_ctor),
15616 }],
15617 )
15618 .await
15619 .unwrap();
15620 assert!(
15621 !src_delta.text.contains("direction"),
15622 "Expected direction argument to be removed from source, got: {}",
15623 src_delta.text
15624 );
15625
15626 ctx.close().await;
15627 mock_ctx.close().await;
15628 }
15629
15630 #[tokio::test(flavor = "multi_thread")]
15631 async fn test_extra_newlines_add_circle() {
15632 let initial_source = "@settings(defaultLengthUnit = mm)
15634
15635sketch001 = sketch(on = XY) {
15636}
15637";
15638
15639 let program = Program::parse(initial_source).unwrap().0.unwrap();
15640 let mut frontend = FrontendState::new();
15641
15642 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
15643 let mock_ctx = ExecutorContext::new_mock(None).await;
15644 let version = Version(0);
15645
15646 frontend.hack_set_program(&ctx, program).await.unwrap();
15647 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15648 let sketch_id = sketch_object.id;
15649
15650 let circle_ctor = CircleCtor {
15651 start: Point2d {
15652 x: Expr::Var(Number {
15653 value: 5.0,
15654 units: NumericSuffix::Mm,
15655 }),
15656 y: Expr::Var(Number {
15657 value: 0.0,
15658 units: NumericSuffix::Mm,
15659 }),
15660 },
15661 center: Point2d {
15662 x: Expr::Var(Number {
15663 value: 0.0,
15664 units: NumericSuffix::Mm,
15665 }),
15666 y: Expr::Var(Number {
15667 value: 0.0,
15668 units: NumericSuffix::Mm,
15669 }),
15670 },
15671 construction: None,
15672 };
15673 let segment = SegmentCtor::Circle(circle_ctor);
15674 let (src_delta, scene_delta) = frontend
15675 .add_segment(&mock_ctx, version, sketch_id, segment, None)
15676 .await
15677 .unwrap();
15678 assert!(
15679 src_delta
15680 .text
15681 .contains("circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])"),
15682 "Expected circle in source, got: {}",
15683 src_delta.text
15684 );
15685 assert!(!scene_delta.new_objects.is_empty());
15686
15687 ctx.close().await;
15688 mock_ctx.close().await;
15689 }
15690
15691 #[tokio::test(flavor = "multi_thread")]
15692 async fn test_extra_newlines_add_constraint() {
15693 let initial_source = "@settings(defaultLengthUnit = mm)
15695
15696sketch001 = sketch(on = XY) {
15697 line1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm])
15698 line2 = line(start = [var 10mm, var 10mm], end = [var 20mm, var 0mm])
15699}
15700";
15701
15702 let program = Program::parse(initial_source).unwrap().0.unwrap();
15703 let mut frontend = FrontendState::new();
15704
15705 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
15706 let mock_ctx = ExecutorContext::new_mock(None).await;
15707 let version = Version(0);
15708 let project_id = ProjectId(0);
15709 let file_id = FileId(0);
15710
15711 frontend.hack_set_program(&ctx, program).await.unwrap();
15712 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15713 let sketch_id = sketch_object.id;
15714 let sketch = expect_sketch(sketch_object);
15715
15716 let line_ids: Vec<ObjectId> = sketch
15718 .segments
15719 .iter()
15720 .copied()
15721 .filter(|seg_id| {
15722 matches!(
15723 &frontend.scene_graph.objects[seg_id.0].kind,
15724 ObjectKind::Segment {
15725 segment: Segment::Line(_)
15726 }
15727 )
15728 })
15729 .collect();
15730 assert_eq!(line_ids.len(), 2, "Expected two line segments");
15731
15732 let line1 = &frontend.scene_graph.objects[line_ids[0].0];
15733 let ObjectKind::Segment {
15734 segment: Segment::Line(line1_data),
15735 } = &line1.kind
15736 else {
15737 panic!("Expected line");
15738 };
15739 let line2 = &frontend.scene_graph.objects[line_ids[1].0];
15740 let ObjectKind::Segment {
15741 segment: Segment::Line(line2_data),
15742 } = &line2.kind
15743 else {
15744 panic!("Expected line");
15745 };
15746
15747 let constraint = Constraint::Coincident(Coincident {
15749 segments: vec![line1_data.end.into(), line2_data.start.into()],
15750 });
15751
15752 frontend
15754 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
15755 .await
15756 .unwrap();
15757 let (src_delta, _scene_delta) = frontend
15758 .add_constraint(&mock_ctx, version, sketch_id, constraint)
15759 .await
15760 .unwrap();
15761 assert!(
15762 src_delta.text.contains("coincident("),
15763 "Expected coincident constraint in source, got: {}",
15764 src_delta.text
15765 );
15766
15767 ctx.close().await;
15768 mock_ctx.close().await;
15769 }
15770
15771 #[tokio::test(flavor = "multi_thread")]
15772 async fn test_extra_newlines_add_line_then_edit_line() {
15773 let initial_source = "@settings(defaultLengthUnit = mm)
15775
15776sketch001 = sketch(on = XY) {
15777}
15778";
15779
15780 let program = Program::parse(initial_source).unwrap().0.unwrap();
15781 let mut frontend = FrontendState::new();
15782
15783 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
15784 let mock_ctx = ExecutorContext::new_mock(None).await;
15785 let version = Version(0);
15786
15787 frontend.hack_set_program(&ctx, program).await.unwrap();
15788 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15789 let sketch_id = sketch_object.id;
15790
15791 let line_ctor = LineCtor {
15793 start: Point2d {
15794 x: Expr::Number(Number {
15795 value: 0.0,
15796 units: NumericSuffix::Mm,
15797 }),
15798 y: Expr::Number(Number {
15799 value: 0.0,
15800 units: NumericSuffix::Mm,
15801 }),
15802 },
15803 end: Point2d {
15804 x: Expr::Number(Number {
15805 value: 10.0,
15806 units: NumericSuffix::Mm,
15807 }),
15808 y: Expr::Number(Number {
15809 value: 10.0,
15810 units: NumericSuffix::Mm,
15811 }),
15812 },
15813 construction: None,
15814 };
15815 let segment = SegmentCtor::Line(line_ctor);
15816 let (src_delta, scene_delta) = frontend
15817 .add_segment(&mock_ctx, version, sketch_id, segment, None)
15818 .await
15819 .unwrap();
15820 assert!(
15821 src_delta.text.contains("line(start = [0mm, 0mm], end = [10mm, 10mm])"),
15822 "Expected line in source after add, got: {}",
15823 src_delta.text
15824 );
15825 let line_id = *scene_delta.new_objects.last().unwrap();
15827
15828 let line_ctor = LineCtor {
15830 start: Point2d {
15831 x: Expr::Number(Number {
15832 value: 1.0,
15833 units: NumericSuffix::Mm,
15834 }),
15835 y: Expr::Number(Number {
15836 value: 2.0,
15837 units: NumericSuffix::Mm,
15838 }),
15839 },
15840 end: Point2d {
15841 x: Expr::Number(Number {
15842 value: 13.0,
15843 units: NumericSuffix::Mm,
15844 }),
15845 y: Expr::Number(Number {
15846 value: 14.0,
15847 units: NumericSuffix::Mm,
15848 }),
15849 },
15850 construction: None,
15851 };
15852 let segments = vec![ExistingSegmentCtor {
15853 id: line_id,
15854 ctor: SegmentCtor::Line(line_ctor),
15855 }];
15856 let (src_delta, scene_delta) = frontend
15857 .edit_segments(&mock_ctx, version, sketch_id, segments)
15858 .await
15859 .unwrap();
15860 assert!(
15861 src_delta.text.contains("line(start = [1mm, 2mm], end = [13mm, 14mm])"),
15862 "Expected edited line in source, got: {}",
15863 src_delta.text
15864 );
15865 assert_eq!(scene_delta.new_objects, vec![]);
15866
15867 ctx.close().await;
15868 mock_ctx.close().await;
15869 }
15870
15871 #[test]
15872 fn test_add_variable_declaration_uses_top_level_scope_after_sketch_block() {
15873 let code = "\
15882foo1 = 1
15883sk = sketch() {
15884 p = var 1.5
15885}
158867 + 8
15887";
15888 let mut ast = crate::parsing::top_level_parse(code).unwrap();
15889 let ast::BodyItem::ExpressionStatement(stmt) = &ast.body[2] else {
15890 panic!("expected an expression statement");
15891 };
15892 let source_ref = SourceRef::new(SourceRange::from(&stmt.expression), None);
15893 let (_, cmd_return) = mutate_ast_node_by_source_ref(
15894 &mut ast,
15895 &source_ref,
15896 AstMutateCommand::AddVariableDeclaration {
15897 prefix: "foo".to_owned(),
15898 },
15899 )
15900 .unwrap();
15901 let AstMutateCommandReturn::Name(name) = cmd_return else {
15902 panic!("expected a generated name");
15903 };
15904 assert_eq!(name, "foo2");
15905 let ast::BodyItem::VariableDeclaration(decl) = &ast.body[2] else {
15906 panic!("expected the expression statement to become a variable declaration");
15907 };
15908 assert_eq!(decl.name(), "foo2");
15909 }
15910
15911 fn function_body_at(ast: &ast::Node<ast::Program>, index: usize) -> &ast::Node<ast::Program> {
15913 let ast::BodyItem::VariableDeclaration(decl) = &ast.body[index] else {
15914 panic!("expected a variable declaration");
15915 };
15916 let ast::Expr::FunctionExpression(func) = &decl.declaration.init else {
15917 panic!("expected a function expression");
15918 };
15919 &func.body
15920 }
15921
15922 fn then_block_at(ast: &ast::Node<ast::Program>, index: usize) -> &ast::Node<ast::Program> {
15925 let ast::BodyItem::VariableDeclaration(decl) = &ast.body[index] else {
15926 panic!("expected a variable declaration");
15927 };
15928 let ast::Expr::IfExpression(if_expr) = &decl.declaration.init else {
15929 panic!("expected an if expression");
15930 };
15931 &if_expr.then_val
15932 }
15933
15934 #[test]
15935 fn test_add_variable_declaration_in_function_body_uses_function_scope() {
15936 let code = "\
15941fn build() {
15942 thing1 = 1
15943 10 + 20
15944 return thing1
15945}
15946";
15947 let mut ast = crate::parsing::top_level_parse(code).unwrap();
15948 let ast::BodyItem::ExpressionStatement(stmt) = &function_body_at(&ast, 0).body[1] else {
15949 panic!("expected an expression statement");
15950 };
15951 let source_ref = SourceRef::new(SourceRange::from(&stmt.expression), None);
15952 let (_, cmd_return) = mutate_ast_node_by_source_ref(
15953 &mut ast,
15954 &source_ref,
15955 AstMutateCommand::AddVariableDeclaration {
15956 prefix: "thing".to_owned(),
15957 },
15958 )
15959 .unwrap();
15960 let AstMutateCommandReturn::Name(name) = cmd_return else {
15961 panic!("expected a generated name");
15962 };
15963 assert_eq!(name, "thing2");
15964 let body = &function_body_at(&ast, 0).body;
15965 assert_eq!(body.len(), 3);
15966 let ast::BodyItem::VariableDeclaration(decl) = &body[1] else {
15967 panic!("expected the expression statement to become a variable declaration");
15968 };
15969 assert_eq!(decl.name(), "thing2");
15970 let ast::BodyItem::VariableDeclaration(first) = &body[0] else {
15972 panic!("expected a variable declaration");
15973 };
15974 assert_eq!(first.name(), "thing1");
15975 assert!(matches!(&body[2], ast::BodyItem::ReturnStatement(_)));
15976 }
15977
15978 #[test]
15979 fn test_delete_node_in_function_body_preserves_leading_comment() {
15980 let code = "\
15984fn build() {
15985 a = 1
15986 // keep me
15987 b = 2
15988 return a
15989}
15990";
15991 let mut ast = crate::parsing::top_level_parse(code).unwrap();
15992 let ast::BodyItem::VariableDeclaration(b_decl) = &function_body_at(&ast, 0).body[1] else {
15993 panic!("expected a variable declaration");
15994 };
15995 assert_eq!(b_decl.name(), "b");
15996 let source_ref = SourceRef::new(SourceRange::from(&b_decl.declaration.init), None);
15997 mutate_ast_node_by_source_ref(&mut ast, &source_ref, AstMutateCommand::DeleteNode).unwrap();
15998 let body = &function_body_at(&ast, 0).body;
15999 assert_eq!(body.len(), 2, "expected b to be deleted");
16000 let ast::BodyItem::VariableDeclaration(first) = &body[0] else {
16001 panic!("expected a variable declaration");
16002 };
16003 assert_eq!(first.name(), "a");
16004 let ast::BodyItem::ReturnStatement(_) = &body[1] else {
16005 panic!("expected the return statement to remain");
16006 };
16007 assert!(
16008 body[1].get_comments().iter().any(|c| c.contains("keep me")),
16009 "expected the deleted item's leading comment to migrate to the next item, got: {:?}",
16010 body[1].get_comments()
16011 );
16012 }
16013
16014 #[test]
16015 fn test_add_variable_declaration_in_function_body_ignores_parameters() {
16016 let code = "\
16024fn build(thing2) {
16025 thing1 = 1
16026 10 + 20
16027 return thing1 + thing2
16028}
16029";
16030 let mut ast = crate::parsing::top_level_parse(code).unwrap();
16031 let ast::BodyItem::ExpressionStatement(stmt) = &function_body_at(&ast, 0).body[1] else {
16032 panic!("expected an expression statement");
16033 };
16034 let source_ref = SourceRef::new(SourceRange::from(&stmt.expression), None);
16035 let (_, cmd_return) = mutate_ast_node_by_source_ref(
16036 &mut ast,
16037 &source_ref,
16038 AstMutateCommand::AddVariableDeclaration {
16039 prefix: "thing".to_owned(),
16040 },
16041 )
16042 .unwrap();
16043 let AstMutateCommandReturn::Name(name) = cmd_return else {
16044 panic!("expected a generated name");
16045 };
16046 assert_eq!(name, "thing2", "locals are avoided, but parameters are not");
16047 }
16048
16049 #[test]
16050 fn test_add_variable_declaration_in_if_branch_uses_branch_scope() {
16051 let code = "\
16052x = 1
16053y = if x > 0 {
16054 q1 = 1
16055 foo(q1)
16056 q1
16057} else {
16058 2
16059}
16060";
16061 let mut ast = crate::parsing::top_level_parse(code).unwrap();
16062 let ast::BodyItem::ExpressionStatement(stmt) = &then_block_at(&ast, 1).body[1] else {
16063 panic!("expected an expression statement");
16064 };
16065 let source_ref = SourceRef::new(SourceRange::from(&stmt.expression), None);
16066 let (_, cmd_return) = mutate_ast_node_by_source_ref(
16067 &mut ast,
16068 &source_ref,
16069 AstMutateCommand::AddVariableDeclaration { prefix: "q".to_owned() },
16070 )
16071 .unwrap();
16072 let AstMutateCommandReturn::Name(name) = cmd_return else {
16073 panic!("expected a generated name");
16074 };
16075 assert_eq!(name, "q2");
16076 let body = &then_block_at(&ast, 1).body;
16077 assert_eq!(body.len(), 3);
16078 let ast::BodyItem::VariableDeclaration(decl) = &body[1] else {
16079 panic!("expected the expression statement to become a variable declaration");
16080 };
16081 assert_eq!(decl.name(), "q2");
16082 let ast::BodyItem::VariableDeclaration(first) = &body[0] else {
16084 panic!("expected a variable declaration");
16085 };
16086 assert_eq!(first.name(), "q1");
16087 assert!(matches!(&body[2], ast::BodyItem::ExpressionStatement(_)));
16088 }
16089
16090 #[test]
16091 fn test_delete_node_in_if_branch_preserves_leading_comment() {
16092 let code = "\
16095y = if true {
16096 a = 1
16097 // keep me
16098 b = 2
16099 a + b
16100} else {
16101 2
16102}
16103";
16104 let mut ast = crate::parsing::top_level_parse(code).unwrap();
16105 let ast::BodyItem::VariableDeclaration(b_decl) = &then_block_at(&ast, 0).body[1] else {
16106 panic!("expected a variable declaration");
16107 };
16108 assert_eq!(b_decl.name(), "b");
16109 let source_ref = SourceRef::new(SourceRange::from(&b_decl.declaration.init), None);
16110 mutate_ast_node_by_source_ref(&mut ast, &source_ref, AstMutateCommand::DeleteNode).unwrap();
16111 let body = &then_block_at(&ast, 0).body;
16112 assert_eq!(body.len(), 2, "expected b to be deleted");
16113 let ast::BodyItem::VariableDeclaration(first) = &body[0] else {
16114 panic!("expected a variable declaration");
16115 };
16116 assert_eq!(first.name(), "a");
16117 let ast::BodyItem::ExpressionStatement(_) = &body[1] else {
16118 panic!("expected the tail expression to remain");
16119 };
16120 assert!(
16121 body[1].get_comments().iter().any(|c| c.contains("keep me")),
16122 "expected the deleted item's leading comment to migrate to the next item, got: {:?}",
16123 body[1].get_comments()
16124 );
16125 }
16126}